diff --git a/packages/solid/src/render/component.ts b/packages/solid/src/render/component.ts index dc6ab4aa6..431de8b13 100644 --- a/packages/solid/src/render/component.ts +++ b/packages/solid/src/render/component.ts @@ -265,6 +265,14 @@ export function mergeProps(...sources: T): MergeProps { return target as any; } +export type DefaultProps = MergeProps<[Required>, T]>; +export function defaultProps( + props: T, + defaults: Required> +): DefaultProps { + return mergeProps(defaults, props); +} + export type SplitProps = [ ...{ [P in keyof K]: P extends `${number}` diff --git a/packages/solid/test/component.spec.ts b/packages/solid/test/component.spec.ts index b28156b7a..14ff12c1d 100644 --- a/packages/solid/test/component.spec.ts +++ b/packages/solid/test/component.spec.ts @@ -2,6 +2,7 @@ import { createRoot, createComponent, mergeProps, + defaultProps, splitProps, createUniqueId, createSignal, @@ -225,21 +226,54 @@ describe("mergeProps", () => { }); }); -describe("Set Default Props", () => { - test("simple set", () => { +describe("defaultProps", () => { + test("empty defaults", () => { let props: SimplePropTypes = { - get a() { - return "ji"; - }, - b: null, - c: "j" + get a() { + return "beep"; }, - defaults: SimplePropTypes = { a: "yy", b: "ggg", d: "DD" }; - props = mergeProps(defaults, props); - expect(props.a).toBe("ji"); + b: null, + c: "boop" + }; + props = defaultProps(props, {}); + expect(props.a).toBe("beep"); expect(props.b).toBe(null); - expect(props.c).toBe("j"); - expect(props.d).toBe("DD"); + expect(props.c).toBe("boop"); + expect(props.d).toBe(undefined); + }); + it("overwrites only undefined values", () => { + let props: SimplePropTypes = { + get a() { + return "beep"; + }, + b: null, + c: "boop" + }; + props = defaultProps(props, { + a: "xxx", + b: "xxx", + c: "xxx", + d: "xxx" + }); + expect(props.a).toBe("beep"); + expect(props.b).toBe(null); + expect(props.c).toBe("boop"); + expect(props.d).toBe("xxx"); + }); + it("allows null as a default", () => { + let props: SimplePropTypes = { + a: "defined", + c: null + }; + props = defaultProps(props, { + a: null, + b: null, + c: null + }); + expect(props.a).toBe("defined"); + expect(props.b).toBe(null); + expect(props.c).toBe(null); + expect(props.d).toBe(undefined); }); }); diff --git a/packages/solid/test/component.type-tests.ts b/packages/solid/test/component.type-tests.ts index 94dee39e3..1a1b6ced6 100644 --- a/packages/solid/test/component.type-tests.ts +++ b/packages/solid/test/component.type-tests.ts @@ -1,4 +1,4 @@ -import { mergeProps, splitProps } from "../src"; +import { mergeProps, defaultProps, splitProps } from "../src"; type Assert = never; // from: https://github.com/Microsoft/TypeScript/issues/27024#issuecomment-421529650 @@ -182,6 +182,54 @@ function M4( type TestM9 = Assert>; } +// d1: defaultProps preserves prop types +type D1Props = { + a?: string; + b: string; + c?: "one" | "two" | "three"; + d: "one" | "two" | "three"; + e?: [1, 2, 3]; + f: [1, 2, 3]; + g?: [string, number, "one" | "two", 3 | 4]; + h: [string, number, "one" | "two", 3 | 4]; + i?: null; + j: null; +}; +const d1 = defaultProps({} as D1Props, {}); +type D1 = typeof d1; +type TestD1 = Assert>; + +// d2: defaultProps removes undefined on merged props +const d2 = defaultProps( + {} as { + a?: string; + b?: "one" | "two" | "three"; + c?: [1, 2, 3]; + d?: [string, number, "one" | "two", 3 | 4]; + e?: null; + }, + { + a: "hello", + b: "two", + c: [1, 2, 3], + d: ["string", 99, "one", 4], + e: null + } +); +type D2 = typeof d2; +type TestD2 = Assert< + IsExact< + D2, + { + a: string; + b: "one" | "two" | "three"; + c: [1, 2, 3]; + d: [string, number, "one" | "two", 3 | 4]; + e: null; + } + > +>; + // s1-s3: splitProps return type is correct regardless of usage const s1 = splitProps({ a: 1, b: 2 }, ["a"]); type S1 = typeof s1;