Skip to content

Commit

Permalink
refactor: improve mergeProps types (#931)
Browse files Browse the repository at this point in the history
* refactor: improve mergeProps return type

- Make `mergeProps` require at least 1 source
- Make the types of properties correct when merging types that would otherwise return `never` when intersected e.g. merging `{ a: number }` and `{ a: string }` is now `{ a: string }` instead of `{ a: never }`
- Add tests for the above

* refactor: ignore non-objects in mergeProps type

* feat: export MergeProps and SplitProps types
  • Loading branch information
otonashixav committed May 12, 2022
1 parent 3777a33 commit b5c5e60
Show file tree
Hide file tree
Showing 2 changed files with 118 additions and 13 deletions.
33 changes: 20 additions & 13 deletions packages/solid/src/render/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,23 +136,30 @@ const propTraps: ProxyHandler<{
}
};

type Simplify<T> = T extends object ? { [K in keyof T]: T[K] } : T;
type UnboxLazy<T> = T extends () => infer U ? U : T;
type BoxedTupleTypes<T extends any[]> = { [P in keyof T]: [UnboxLazy<T[P]>] }[Exclude<
keyof T,
keyof any[]
>];
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (k: infer I) => void
? I
: never;
type UnboxIntersection<T> = T extends { 0: infer U } ? U : never;
type MergeProps<T extends any[]> = UnboxIntersection<UnionToIntersection<BoxedTupleTypes<T>>>;
type RequiredKeys<T> = keyof { [K in keyof T as T extends { [_ in K]: unknown } ? K : never]: 0 };
type Override<T, U> = {
// all keys in T which are not overridden by U
[K in keyof Omit<T, RequiredKeys<U>>]: T[K] | Exclude<U[K & keyof U], undefined>;
} & {
// all keys in U except those which are merged into T
[K in keyof Omit<U, Exclude<keyof T, RequiredKeys<U>>>]:
| Exclude<U[K], undefined>
| (undefined extends U[K] ? (K extends keyof T ? T[K] : undefined) : never);
};
export type MergeProps<T extends unknown[], Curr = {}> = T extends [infer Next, ...infer Rest]
? MergeProps<
Rest,
Next extends object ? (Next extends Function ? Curr : Override<Curr, UnboxLazy<Next>>) : Curr
>
: Simplify<Curr>;

function resolveSource(s: any) {
return (s = typeof s === "function" ? s() : s) == null ? {} : s;
}

export function mergeProps<T extends any[]>(...sources: T): MergeProps<T>;
export function mergeProps(...sources: any): any {
export function mergeProps<T extends [unknown, ...unknown[]]>(...sources: T): MergeProps<T> {
return new Proxy(
{
get(property: string | number | symbol) {
Expand All @@ -175,10 +182,10 @@ export function mergeProps(...sources: any): any {
}
},
propTraps
);
) as unknown as MergeProps<T>;
}

type SplitProps<T, K extends (readonly (keyof T)[])[]> = [
export type SplitProps<T, K extends (readonly (keyof T)[])[]> = [
...{
[P in keyof K]: P extends `${number}`
? Pick<T, Extract<K[P], readonly (keyof T)[]>[number]>
Expand Down
98 changes: 98 additions & 0 deletions packages/solid/test/component.type-tests.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { mergeProps } from "../src";

type Assert<T extends true> = never;
type Not<T extends boolean> = [T] extends [true] ? false : true;
// from: https://github.com/Microsoft/TypeScript/issues/27024#issuecomment-421529650
type IsExact<T, U, I = never> = (<G>() => G extends T | I ? 1 : 2) extends <G>() => G extends U | I
? 1
: 2
? true
: false;
type IsRequired<T, K extends keyof T> = T extends { [_ in K]: unknown } ? true : false;
type ExactOptionalPropertyType<T, K extends keyof T> = T extends { [_ in K]?: infer I } ? I : never;
type IsOptionalProperty<T, K extends keyof T, P> = IsExact<
ExactOptionalPropertyType<T, K>,
P,
[undefined] extends [0?] ? undefined : never
> extends true
? Not<IsRequired<T, K>>
: false;
type IsRequiredProperty<T, K extends keyof T, P> = IsExact<T[K], P> extends true
? IsRequired<T, K>
: false;

// normal merge cases
const m1 = mergeProps(
{} as {
a: number;
b: number;
c: number;
d?: number;
e?: number;
f?: number;
i: number;
j?: number;
m: undefined;
n: undefined;
o?: undefined;
p: number;
q: 1;
r: number;
s: 1;
},
{} as {
b: string;
c?: string;
e: string;
f?: string;
g: string;
h?: string;
i: undefined;
j: undefined;
k: undefined;
l?: undefined;
m: string;
n?: string;
o?: string;
p: 1;
q: number;
r?: 1;
s?: number;
}
);
type M1 = typeof m1;
type TestM1 =
| Assert<IsRequiredProperty<M1, "a", number>>
| Assert<IsRequiredProperty<M1, "b", string>>
| Assert<IsRequiredProperty<M1, "c", string | number>>
| Assert<IsOptionalProperty<M1, "d", number>>
| Assert<IsRequiredProperty<M1, "e", string>>
| Assert<IsOptionalProperty<M1, "f", string | number>>
| Assert<IsRequiredProperty<M1, "g", string>>
| Assert<IsOptionalProperty<M1, "h", string>>
| Assert<IsRequiredProperty<M1, "i", number>>
| Assert<IsRequiredProperty<M1, "j", number | undefined>>
| Assert<IsRequiredProperty<M1, "k", undefined>>
| Assert<IsOptionalProperty<M1, "l", undefined>>
| Assert<IsRequiredProperty<M1, "m", string>>
| Assert<IsRequiredProperty<M1, "n", string | undefined>>
| Assert<IsOptionalProperty<M1, "o", string | undefined>>
| Assert<IsRequiredProperty<M1, "p", 1>>
| Assert<IsRequiredProperty<M1, "q", number>>
| Assert<IsRequiredProperty<M1, "r", number>>
| Assert<IsRequiredProperty<M1, "s", number>>;

const m2 = mergeProps({ a: 1 } as { a?: number }, { a: 1 } as { a?: number });
type M2 = typeof m2;
type TestM2 = Assert<IsOptionalProperty<M2, "a", number>>;

const m3 = mergeProps({ a: 1 }, { a: undefined });
type M3 = typeof m3;
type TestM3 = Assert<IsRequiredProperty<M3, "a", number>>;

const m4 = mergeProps({ a: 1 }, 1, null, undefined, () => 1, "", 3, { a: 1 });
type M4 = typeof m4;
type TestM4 = Assert<IsExact<M4, { a: number }>>;

// @ts-expect-error mergeProps requires at least one param
mergeProps();

0 comments on commit b5c5e60

Please sign in to comment.