Skip to content

Commit

Permalink
feat: strict the types for the ex API and add tests (#18)
Browse files Browse the repository at this point in the history
  • Loading branch information
taishinaritomi committed Aug 16, 2023
1 parent dddf17b commit f0d4886
Show file tree
Hide file tree
Showing 4 changed files with 158 additions and 58 deletions.
5 changes: 5 additions & 0 deletions .changeset/two-plums-matter.md
@@ -0,0 +1,5 @@
---
"excss": patch
---

feat: strict the types for the ex API and add tests
67 changes: 60 additions & 7 deletions packages/excss/src/ex.spec.ts
Expand Up @@ -15,20 +15,73 @@ describe("ex", () => {
1: "number-1",
},
boolean: {
true: "is-true",
false: "is-false",
true: "boolean-true",
false: "boolean-false",
},
});

expect(style({ number: 1 })).equals("number-1 is-true");
expect(style({ number: 1, boolean: undefined })).equals(
"number-1 boolean-true",
);

type Style = {
expectTypeOf(style).toEqualTypeOf<
(props?: {
string?: "red" | "blue";
number?: 0 | 1;
boolean?: boolean;
}) => string
>();

/// Ex

expectTypeOf<Ex<typeof style>>().toEqualTypeOf<{
string?: "red" | "blue";
number?: 0 | 1;
boolean?: boolean;
}>();

/// Ex.Required

expectTypeOf<Ex.Required<typeof style>>().toEqualTypeOf<{
string: "red" | "blue";
number: 0 | 1;
boolean: boolean;
}>();

expectTypeOf<
Ex.Required<typeof style, "string" | "number">
>().toEqualTypeOf<{
string?: "red" | "blue";
number?: 0 | 1;
boolean: boolean;
}>();

expectTypeOf<Ex.Required<Ex<typeof style>>>().toEqualTypeOf<{
string: "red" | "blue";
number: 0 | 1;
boolean: boolean;
}>();

/// Ex.Optional

expectTypeOf<Ex.Optional<typeof style>>().toEqualTypeOf<{
string?: "red" | "blue";
number?: 0 | 1;
boolean?: boolean;
};
}>();

expectTypeOf(style).toEqualTypeOf<(props: Style) => string>();
expectTypeOf<Ex<typeof style>>().toEqualTypeOf<Style>();
expectTypeOf<
Ex.Optional<typeof style, "string" | "number">
>().toEqualTypeOf<{
string: "red" | "blue";
number: 0 | 1;
boolean?: boolean;
}>();

expectTypeOf<Ex.Optional<Ex<typeof style>>>().toEqualTypeOf<{
string?: "red" | "blue";
number?: 0 | 1;
boolean?: boolean;
}>();
});
});
127 changes: 76 additions & 51 deletions packages/excss/src/ex.ts
@@ -1,77 +1,102 @@
import type { Pretty, ToLiteral } from "./utils/types";
import type {
Pretty,
ToLiteral,
Required as _Required,
Optional as _Optional,
} from "./utils/types";

export { ex };
export type { Ex };

const DEFAULT = "default";

namespace Ex {
type StdRequired<T> = { [P in keyof T]-?: T[P] };
type _Required<T, K extends keyof T = keyof T> = Omit<
T & StdRequired<Pick<T, K & keyof T>>,
never
>;

export type ClassName = ClassName[] | string | false | null | undefined;

export type Key = string | boolean | number | null | undefined;

export type Required<
T extends RawCallback,
K extends keyof Ex<T> = keyof Ex<T>,
> = _Required<Ex<T>, K>;
T extends RawCallback | RawProps,
Optional extends T extends RawCallback
? keyof Ex<T>
: Exclude<keyof T, undefined> = never,
> = T extends RawCallback
? _Required<Ex<T>, Optional>
: _Required<
T,
Optional extends Exclude<keyof T, undefined> ? Optional : never
>;

export type Optional<
T extends RawCallback | RawProps,
Required extends T extends RawCallback
? keyof Ex<T>
: Exclude<keyof T, undefined> = never,
> = T extends RawCallback
? _Optional<Ex<T>, Required>
: _Optional<
T,
Required extends Exclude<keyof T, undefined> ? Required : never
>;
}

export type Props<T> = Pretty<{
[K in Exclude<keyof T, typeof DEFAULT>]?: ToLiteral<
keyof T[K],
Exclude<Key, string>
>;
}>;
type Ex<T extends RawCallback> = Pretty<Exclude<Parameters<T>[0], undefined>>;

export type Input<T> = Pretty<{
[DEFAULT]?: Props<T>;
}> & {
[K in keyof T]?: K extends typeof DEFAULT ? Props<T> : Selectors<T[K]>;
};
function ex<T>(input: Input<T>) {
const bind = { _input: input as RawInput };
return callback.bind(bind) as (props?: Props<T> | undefined) => string;
}

type Selectors<T> = Pretty<{
[K in keyof T]: T[K] extends string ? T[K] : never;
}>;
ex.join = function (...classNames: Ex.ClassName[]) {
return resolveClassName(classNames);
};

type RawDefault = Record<string, Key>;
type RawSelector = Record<string, string>;
type Props<T> = Pretty<{
[K in Exclude<keyof T, typeof DEFAULT>]?:
| ToLiteral<keyof T[K], Exclude<RawKey, string>>
| undefined;
}>;

type Input<T> = Pretty<
{
[DEFAULT]?: Props<T> | undefined;
} & {
[K in keyof T]?: K extends typeof DEFAULT
? Props<T> | undefined
: Selectors<T[K]>;
}
>;

export type RawInput = Partial<
Record<typeof DEFAULT, RawDefault> & Record<string, RawSelector>
>;
type Selectors<T> = Pretty<{
[K in keyof T]: T[K] extends string ? T[K] : never;
}>;

export type RawProps = Record<string, Key>;
type RawKey = string | boolean | number;

export type RawCallback = (props: RawProps) => string;
}
type RawDefault = Record<string, RawKey>;
type RawSelector = Record<string, string>;

type Ex<T extends Ex.RawCallback> = Pretty<Parameters<T>[0]>;
type RawInput = Partial<
Record<typeof DEFAULT, RawDefault> & Record<string, RawSelector>
>;

function ex<T>(input: Ex.Input<T>) {
const bind = { _input: input as Ex.RawInput };
return callback.bind(bind) as (props: Ex.Props<T>) => string;
}
type RawProps = Record<string, RawKey | undefined> | undefined;

ex.join = function (...classNames: Ex.ClassName[]) {
return resolveClassName(classNames);
};
type RawCallback = (props?: RawProps) => string;

function callback(this: { _input: Ex.RawInput }, props: Ex.RawProps): string {
function callback(this: { _input: RawInput }, props?: RawProps): string {
let result = "";

for (const selector in this._input) {
if (selector !== DEFAULT) {
const key = props[selector] ?? this._input.default?.[selector];
const className = this._input[selector]?.[`${key}`];

if (className) {
if (result) result += " ";
result += className;
if (props) {
for (const selector in this._input) {
if (selector !== DEFAULT) {
const key = props[selector] ?? this._input.default?.[selector];
if (key !== undefined) {
const className = this._input[selector]?.[`${key}`];

if (className) {
if (result) result += " ";
result += className;
}
}
}
}
}
Expand Down
17 changes: 17 additions & 0 deletions packages/excss/src/utils/types.ts
Expand Up @@ -8,3 +8,20 @@ export type ToLiteral<T, L extends Literal> = T extends `${infer R extends L}`
export type Pretty<T> = { [P in keyof T]: T[P] } & {};

export type If<Q extends boolean, T, F> = Q extends true ? T : F;

type TSRequired<T> = { [P in keyof T]-?: T[P] };
type ExcludeValue<T, V> = { [P in keyof T]: Exclude<T[P], V> };
type ExtendValue<T, V> = { [P in keyof T]: T[P] | V };

type StdRequired<T> = ExcludeValue<TSRequired<T>, undefined>;
type StdOptional<T> = ExtendValue<Partial<T>, undefined>;

export type Required<T, Optional extends keyof T = never> = Pretty<
StdOptional<Pick<T, Optional>> &
StdRequired<Pick<T, Exclude<keyof T, Optional>>>
>;

export type Optional<T, Required extends keyof T = never> = Pretty<
StdRequired<Pick<T, Required>> &
StdOptional<Pick<T, Exclude<keyof T, Required>>>
>;

0 comments on commit f0d4886

Please sign in to comment.