Skip to content

Commit

Permalink
Enable Typescript to understand JSX default props
Browse files Browse the repository at this point in the history
See microsoft/TypeScript#24422

This addition allows a Component to use defaultProps without having to
declare them as optional, in a nutshell:

Before

class Before extends Component<{ prop?: string }> {
	static defaultProps = {
		prop: "default value"
	};

	render() {
		// this.props.prop is string|undefined
	}
}

const element = <Before />;

After

class After extends Component<{ prop: string }> {
	static defaultProps = {
		prop: "default value"
	};

	render() {
		// typeof this.props.prop is string
	}
}

const element = <After />;

The definition isn't perfect, it doesn't quite understand type unions where
the type of a single property changes, e.g.

{ type: "number"; value: number } | { type: "string"; value: string }

But this case doesn't break, it just would require you to still provide
a property.

There might be some more things we can do with LibraryManagedAttributes,
it could allow the children property to be correct within Components
(always an Array) whilst still allowing components to specify the type
of children they accept.
  • Loading branch information
Alexendoo committed Aug 10, 2018
1 parent 8dea9cc commit 8f9b829
Show file tree
Hide file tree
Showing 3 changed files with 80 additions and 1 deletion.
2 changes: 1 addition & 1 deletion package.json
Expand Up @@ -119,7 +119,7 @@
"rollup-plugin-node-resolve": "^3.0.0",
"sinon": "^4.4.2",
"sinon-chai": "^3.0.0",
"typescript": "^2.9.0-rc",
"typescript": "^3.0.1",
"uglify-js": "^2.7.5",
"webpack": "^4.3.0"
},
Expand Down
14 changes: 14 additions & 0 deletions src/preact.d.ts
Expand Up @@ -124,6 +124,15 @@ declare namespace preact {
};
}

type Defaultize<Props, Defaults> =
// Distribute over unions
Props extends any
? // Make any properties included in Default optional
& Partial<Pick<Props, Extract<keyof Props, keyof Defaults>>>
// Include the remaining properties from Props
& Pick<Props, Exclude<keyof Props, keyof Defaults>>
: never;

declare global {
namespace JSX {
interface Element extends preact.VNode<any> {
Expand All @@ -140,6 +149,11 @@ declare global {
children: any;
}

type LibraryManagedAttributes<Component, Props> =
Component extends { defaultProps: infer Defaults }
? Defaultize<Props, Defaults>
: Props;

interface SVGAttributes extends HTMLAttributes {
accentHeight?: number | string;
accumulate?: "none" | "sum";
Expand Down
65 changes: 65 additions & 0 deletions test/ts/preact.tsx
Expand Up @@ -125,3 +125,68 @@ class ComponentWithLifecycle extends Component<DummyProps, DummyState> {
console.log("componentDidUpdate", previousProps, previousState, previousContext);
}
}

// Default props: JSX.LibraryManagedAttributes

class DefaultProps extends Component<{text: string, bool: boolean}> {
static defaultProps = {
text: "hello"
};

render() {
return <div>{this.props.text}</div>;
}
}

const d1 = <DefaultProps bool={false} text="foo" />;
const d2 = <DefaultProps bool={false} />;

class DefaultPropsWithUnion extends Component<
{ default: boolean } & (
| {
type: "string";
str: string;
}
| {
type: "number";
num: number;
})
> {
static defaultProps = {
default: true
};

render() {
return <div />;
}
}

const d3 = <DefaultPropsWithUnion type="string" str={"foo"} />;
const d4 = <DefaultPropsWithUnion type="number" num={0xf00} />;
const d5 = <DefaultPropsWithUnion type="string" str={"foo"} default={false} />;
const d6 = <DefaultPropsWithUnion type="number" num={0xf00} default={false} />;

class DefaultUnion extends Component<
| {
type: "number";
num: number;
}
| {
type: "string";
str: string;
}
> {
static defaultProps = {
type: "number",
num: 1
};

render() {
return <div />;
}
}

const d7 = <DefaultUnion />;
const d8 = <DefaultUnion num={1} />;
const d9 = <DefaultUnion type="number" />;
const d10 = <DefaultUnion type="string" str="foo" />;

0 comments on commit 8f9b829

Please sign in to comment.