Skip to content

Commit ffc8351

Browse files
ferdaberjohnnyreilly
authored andcommitted
[prop-types] use conditional types for better prop type inference (DefinitelyTyped#27378)
* feat(prop-types): use conditional types for better prop type inference * fix(tests): fix publish tests * fix(prop-types): add custom prop validator, and switch requireables * fix(test): revert ReactFragment change * fix(prop-types): namespace imports from react like a good boy * CR changes * actually remove param from validator * remove anyvalidationmap * everyday i'm test...ering * SEMICOLONS WHY * retain null in undefaultize
1 parent ec4917b commit ffc8351

File tree

15 files changed

+249
-146
lines changed

15 files changed

+249
-146
lines changed

types/material-ui-pagination/index.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// Project: https://github.com/lo-tp/material-ui-pagination
33
// Definitions by: m0a <https://m0a.github.io>
44
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
5-
// TypeScript Version: 2.6
5+
// TypeScript Version: 2.8
66
import * as React from 'react';
77
export interface PaginationProps {
88
total: number;

types/material-ui/index.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
// Sam Walsh <https://github.com/samwalshnz>
1515
// Tim de Koning <https://github.com/reggino>
1616
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
17-
// TypeScript Version: 2.6
17+
// TypeScript Version: 2.8
1818

1919
/// <reference types="react" />
2020
/// <reference types="react-addons-linked-state-mixin" />

types/ngreact/index.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// Project: https://github.com/ngReact/ngReact
33
// Definitions by: Vicky Lai <https://github.com/velveret>
44
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
5-
// TypeScript Version: 2.6
5+
// TypeScript Version: 2.8
66

77
/// <reference types="angular"/>
88
/// <reference types="react"/>

types/prop-types/index.d.ts

Lines changed: 39 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,52 @@
11
// Type definitions for prop-types 15.5
22
// Project: https://github.com/reactjs/prop-types
33
// Definitions by: DovydasNavickas <https://github.com/DovydasNavickas>
4+
// Ferdy Budhidharma <https://github.com/ferdaber>
45
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
5-
// TypeScript Version: 2.2
6+
// TypeScript Version: 2.8
67

7-
export type Validator<T> = (object: T, key: string, componentName: string, ...rest: any[]) => Error | null;
8+
import { ReactNode, ReactElement } from 'react';
89

9-
export interface Requireable<T> extends Validator<T> {
10-
isRequired: Validator<T>;
10+
export const nominalTypeHack: unique symbol;
11+
12+
export type IsOptional<T> = undefined | null extends T ? true : undefined extends T ? true : null extends T ? true : false;
13+
14+
export type RequiredKeys<V> = { [K in keyof V]: V[K] extends Validator<infer T> ? IsOptional<T> extends true ? never : K : never }[keyof V];
15+
export type OptionalKeys<V> = Exclude<keyof V, RequiredKeys<V>>;
16+
export type InferPropsInner<V> = { [K in keyof V]: InferType<V[K]>; };
17+
18+
export interface Validator<T> {
19+
(props: object, propName: string, componentName: string, location: string, propFullName: string): Error | null;
20+
[nominalTypeHack]?: T;
1121
}
1222

13-
export type ValidationMap<T> = {[K in keyof T]?: Validator<T> };
23+
export interface Requireable<T> extends Validator<T | undefined | null> {
24+
isRequired: Validator<NonNullable<T>>;
25+
}
26+
27+
export type ValidationMap<T> = { [K in keyof T]-?: Validator<T[K]> };
28+
29+
export type InferType<V> = V extends Validator<infer T> ? T : any;
30+
export type InferProps<V> =
31+
& InferPropsInner<Pick<V, RequiredKeys<V>>>
32+
& Partial<InferPropsInner<Pick<V, OptionalKeys<V>>>>;
1433

1534
export const any: Requireable<any>;
16-
export const array: Requireable<any>;
17-
export const bool: Requireable<any>;
18-
export const func: Requireable<any>;
19-
export const number: Requireable<any>;
20-
export const object: Requireable<any>;
21-
export const string: Requireable<any>;
22-
export const node: Requireable<any>;
23-
export const element: Requireable<any>;
24-
export const symbol: Requireable<any>;
25-
export function instanceOf(expectedClass: {}): Requireable<any>;
26-
export function oneOf(types: any[]): Requireable<any>;
27-
export function oneOfType(types: Array<Validator<any>>): Requireable<any>;
28-
export function arrayOf(type: Validator<any>): Requireable<any>;
29-
export function objectOf(type: Validator<any>): Requireable<any>;
30-
export function shape(type: ValidationMap<any>): Requireable<any>;
35+
export const array: Requireable<any[]>;
36+
export const bool: Requireable<boolean>;
37+
export const func: Requireable<(...args: any[]) => any>;
38+
export const number: Requireable<number>;
39+
export const object: Requireable<object>;
40+
export const string: Requireable<string>;
41+
export const node: Requireable<ReactNode>;
42+
export const element: Requireable<ReactElement<any>>;
43+
export const symbol: Requireable<symbol>;
44+
export function instanceOf<T>(expectedClass: new (...args: any[]) => T): Requireable<T>;
45+
export function oneOf<T>(types: T[]): Requireable<T>;
46+
export function oneOfType<T extends Validator<any>>(types: T[]): Requireable<NonNullable<InferType<T>>>;
47+
export function arrayOf<T>(type: Validator<T>): Requireable<T[]>;
48+
export function objectOf<T>(type: Validator<T>): Requireable<{ [K in keyof any]: T; }>;
49+
export function shape<P extends ValidationMap<any>>(type: P): Requireable<InferProps<P>>;
3150

3251
/**
3352
* Assert that the values match with the type specs.

types/prop-types/prop-types-tests.ts

Lines changed: 189 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,207 @@
1+
import { ReactElement, ReactNode } from "react";
12
import * as PropTypes from "prop-types";
23

4+
declare const uniqueType: unique symbol;
5+
6+
class TestClass { }
7+
38
interface Props {
4-
any: any;
9+
any?: any;
510
array: string[];
611
bool: boolean;
7-
func: any;
8-
string: string;
12+
element: ReactElement<any>;
13+
func(foo: string): void;
14+
node?: ReactNode;
15+
requiredNode: NonNullable<ReactNode>;
916
number: number;
17+
object: object;
18+
string: string;
1019
symbol: symbol;
11-
object: {};
12-
node: any;
13-
element: any;
20+
instanceOf: TestClass;
21+
oneOf: 'a' | 'b' | 'c';
22+
oneOfType: string | boolean | {
23+
foo?: string;
24+
bar: number;
25+
};
26+
numberOrFalse: false | number;
27+
nodeOrRenderFn?: ReactNode | (() => ReactNode);
28+
arrayOf: boolean[];
29+
objectOf: { [K: string]: number };
30+
shape: {
31+
foo: string;
32+
bar?: boolean;
33+
baz?: any
34+
};
35+
optionalNumber?: number | null;
36+
customProp?: typeof uniqueType;
1437
}
1538

16-
const propTypes: PropTypes.ValidationMap<Props> = {
17-
any: PropTypes.any.isRequired,
39+
const innerProps = {
40+
foo: PropTypes.string.isRequired,
41+
bar: PropTypes.bool,
42+
baz: PropTypes.any
43+
};
44+
45+
const arrayOfTypes = [PropTypes.string, PropTypes.bool, PropTypes.shape({
46+
foo: PropTypes.string,
47+
bar: PropTypes.number.isRequired
48+
})];
49+
type PropTypesMap = PropTypes.ValidationMap<Props>;
50+
51+
// TS checking
52+
const propTypes: PropTypesMap = {
53+
any: PropTypes.any,
1854
array: PropTypes.array.isRequired,
1955
bool: PropTypes.bool.isRequired,
56+
element: PropTypes.element.isRequired,
2057
func: PropTypes.func.isRequired,
58+
node: PropTypes.node,
59+
requiredNode: PropTypes.node.isRequired,
2160
number: PropTypes.number.isRequired,
2261
object: PropTypes.object.isRequired,
2362
string: PropTypes.string.isRequired,
2463
symbol: PropTypes.symbol.isRequired,
25-
node: PropTypes.node.isRequired,
26-
element: PropTypes.element.isRequired
64+
instanceOf: PropTypes.instanceOf(TestClass).isRequired,
65+
oneOf: PropTypes.oneOf<'a' | 'b' | 'c'>(['a', 'b', 'c']).isRequired,
66+
oneOfType: PropTypes.oneOfType(arrayOfTypes).isRequired,
67+
numberOrFalse: PropTypes.oneOfType([PropTypes.oneOf<false>([false]), PropTypes.number]).isRequired,
68+
// The generic function type (() => any) is assignable to ReactNode because ReactNode extends the empty object type {}
69+
// Which widens the array literal of validators to just Array<Requireable<() => any>>
70+
// It's too risky to change ReactNode to exclude {} even though it's invalid, as it's required for children-as-function props to work
71+
// So we assert the explicit tuple type
72+
nodeOrRenderFn: PropTypes.oneOfType([PropTypes.node, PropTypes.func] as [PropTypes.Requireable<ReactNode>, PropTypes.Requireable<() => any>]),
73+
arrayOf: PropTypes.arrayOf(PropTypes.bool.isRequired).isRequired,
74+
objectOf: PropTypes.objectOf(PropTypes.number.isRequired).isRequired,
75+
shape: PropTypes.shape(innerProps).isRequired,
76+
optionalNumber: PropTypes.number,
77+
customProp: (() => null) as PropTypes.Validator<typeof uniqueType | undefined>
2778
};
2879

29-
PropTypes.checkPropTypes({xs: PropTypes.array}, {xs: []}, 'location', 'componentName');
80+
// JS checking
81+
const propTypesWithoutAnnotation = {
82+
any: PropTypes.any,
83+
array: PropTypes.array.isRequired,
84+
bool: PropTypes.bool.isRequired,
85+
element: PropTypes.element.isRequired,
86+
func: PropTypes.func.isRequired,
87+
node: PropTypes.node,
88+
requiredNode: PropTypes.node.isRequired,
89+
number: PropTypes.number.isRequired,
90+
object: PropTypes.object.isRequired,
91+
string: PropTypes.string.isRequired,
92+
symbol: PropTypes.symbol.isRequired,
93+
instanceOf: PropTypes.instanceOf(TestClass).isRequired,
94+
// required generic specification because of array type widening
95+
oneOf: PropTypes.oneOf<'a' | 'b' | 'c'>(['a', 'b', 'c']).isRequired,
96+
oneOfType: PropTypes.oneOfType(arrayOfTypes).isRequired,
97+
numberOrFalse: PropTypes.oneOfType([PropTypes.oneOf<false>([false]), PropTypes.number]).isRequired,
98+
nodeOrRenderFn: PropTypes.oneOfType([PropTypes.node, PropTypes.func] as [PropTypes.Requireable<ReactNode>, PropTypes.Requireable<() => any>]),
99+
arrayOf: PropTypes.arrayOf(PropTypes.bool.isRequired).isRequired,
100+
objectOf: PropTypes.objectOf(PropTypes.number.isRequired).isRequired,
101+
shape: PropTypes.shape(innerProps).isRequired,
102+
optionalNumber: PropTypes.number,
103+
customProp: (() => null) as PropTypes.Validator<typeof uniqueType | undefined>
104+
};
105+
106+
const partialPropTypes = {
107+
number: PropTypes.number.isRequired,
108+
object: PropTypes.object.isRequired,
109+
string: PropTypes.string.isRequired,
110+
symbol: PropTypes.symbol.isRequired,
111+
};
112+
113+
const outerPropTypes = {
114+
props: PropTypes.shape(propTypes).isRequired
115+
};
116+
117+
const outerPropTypesWithoutAnnotation = {
118+
props: PropTypes.shape(propTypesWithoutAnnotation).isRequired
119+
};
120+
121+
type ExtractedArrayProps = PropTypes.InferType<(typeof arrayOfTypes)[number]>;
122+
123+
type ExtractedInnerProps = PropTypes.InferProps<typeof innerProps>;
124+
125+
type ExtractedProps = PropTypes.InferProps<typeof propTypes>;
126+
type ExtractedPropsFromOuterProps = PropTypes.InferProps<typeof outerPropTypes>['props'];
127+
type ExtractedPartialProps = PropTypes.InferProps<typeof partialPropTypes>;
128+
129+
type ExtractedPropsWithoutAnnotation = PropTypes.InferProps<typeof propTypesWithoutAnnotation>;
130+
type ExtractedPropsFromOuterPropsWithoutAnnotation = PropTypes.InferProps<typeof outerPropTypesWithoutAnnotation>['props'];
131+
132+
// $ExpectType: true
133+
type ExtractPropsMatch = ExtractedProps extends ExtractedPropsWithoutAnnotation ? true : false;
134+
// $ExpectType: true
135+
type ExtractPropsMatch2 = ExtractedPropsWithoutAnnotation extends ExtractedProps ? true : false;
136+
// $ExpectType: true
137+
type ExtractPropsMatch3 = ExtractedProps extends Props ? true : false;
138+
// $ExpectType: true
139+
type ExtractPropsMatch4 = Props extends ExtractedPropsWithoutAnnotation ? true : false;
140+
// $ExpectType: true
141+
type ExtractFromOuterPropsMatch = ExtractedPropsFromOuterProps extends ExtractedPropsFromOuterPropsWithoutAnnotation ? true : false;
142+
// $ExpectType: true
143+
type ExtractFromOuterPropsMatch2 = ExtractedPropsFromOuterPropsWithoutAnnotation extends ExtractedPropsFromOuterProps ? true : false;
144+
// $ExpectType: true
145+
type ExtractFromOuterPropsMatch3 = ExtractedPropsFromOuterProps extends Props ? true : false;
146+
// $ExpectType: true
147+
type ExtractFromOuterPropsMatch4 = Props extends ExtractedPropsFromOuterPropsWithoutAnnotation ? true : false;
148+
149+
// $ExpectType: false
150+
type ExtractPropsMismatch = ExtractedPartialProps extends Props ? true : false;
151+
152+
// $ExpectType: {}
153+
type UnmatchedPropKeys = Pick<ExtractedPropsWithoutAnnotation, Extract<{
154+
[K in keyof ExtractedPropsWithoutAnnotation]: ExtractedPropsWithoutAnnotation[K] extends ExtractedProps[K] ? never : K
155+
}[keyof ExtractedPropsWithoutAnnotation], keyof ExtractedPropsWithoutAnnotation>>;
156+
// $ExpectType: {}
157+
type UnmatchedPropKeys2 = Pick<ExtractedProps, Extract<{
158+
[K in keyof ExtractedProps]: ExtractedProps[K] extends ExtractedPropsWithoutAnnotation[K] ? never : K
159+
}[keyof ExtractedProps], keyof ExtractedProps>>;
160+
161+
PropTypes.checkPropTypes({ xs: PropTypes.array }, { xs: [] }, 'location', 'componentName');
162+
163+
// This would be the type that JSX sees
164+
type Defaultize<T, D> =
165+
& Pick<T, Exclude<keyof T, keyof D>>
166+
& Partial<Pick<T, Extract<keyof T, keyof D>>>
167+
& Partial<Pick<D, Exclude<keyof D, keyof T>>>;
168+
169+
// This would be the type inside the component
170+
type Undefaultize<T, D> =
171+
& Pick<T, Exclude<keyof T, keyof D>>
172+
& { [K in Extract<keyof T, keyof D>]-?: Exclude<T[K], undefined>; }
173+
& Required<Pick<D, Exclude<keyof D, keyof T>>>;
174+
175+
const componentPropTypes = {
176+
fi: PropTypes.func.isRequired,
177+
foo: PropTypes.string,
178+
bar: PropTypes.number.isRequired,
179+
baz: PropTypes.bool,
180+
bat: PropTypes.node
181+
};
182+
183+
const componentDefaultProps = {
184+
fi: () => null,
185+
baz: false,
186+
bat: ['This', 'is', 'a', 'string']
187+
};
188+
189+
type DefaultizedProps = Defaultize<PropTypes.InferProps<typeof componentPropTypes>, typeof componentDefaultProps>;
190+
type UndefaultizedProps = Undefaultize<PropTypes.InferProps<typeof componentPropTypes>, typeof componentDefaultProps>;
191+
192+
// $ExpectType: true
193+
type DefaultizedPropsTest = {
194+
fi?: (...args: any[]) => any;
195+
foo?: string | null;
196+
bar: number;
197+
baz?: boolean | null;
198+
bat?: ReactNode;
199+
} extends DefaultizedProps ? true : false;
200+
// $ExpectType: true
201+
type UndefaultizedPropsTest = {
202+
fi: (...args: any[]) => any;
203+
foo?: string | null;
204+
bar: number;
205+
baz: boolean;
206+
bat: Exclude<ReactNode, undefined>;
207+
} extends UndefaultizedProps ? true : false;

types/react-foundation/index.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// Project: https://github.com/digiaonline/react-foundation
33
// Definitions by: Daniel Earwicker <https://github.com/danielearwicker>
44
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
5-
// TypeScript Version: 2.6
5+
// TypeScript Version: 2.8
66

77
export { Accordion, AccordionItem, AccordionTitle, AccordionContent } from './components/accordion';
88
export { Badge } from './components/badge';

types/react-is-deprecated/index.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// Project: https://github.com/Aweary/react-is-deprecated
33
// Definitions by: Sean Kelley <https://github.com/seansfkelley>
44
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
5-
// TypeScript Version: 2.6
5+
// TypeScript Version: 2.8
66

77
declare module 'react-is-deprecated' {
88
import { Validator, Requireable, ValidationMap, ReactPropTypes } from 'react';

types/react-leaflet/index.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// Project: https://github.com/PaulLeCam/react-leaflet
33
// Definitions by: Dave Leaver <https://github.com/danzel>, David Schneider <https://github.com/davschne>, Yui T. <https://github.com/yuit>
44
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
5-
// TypeScript Version: 2.6
5+
// TypeScript Version: 2.8
66

77
import * as Leaflet from 'leaflet';
88
import * as React from 'react';

types/react-props-decorators/index.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// Project: https://github.com/popkirby/react-props-decorators
33
// Definitions by: Qubo <https://github.com/tkqubo>
44
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
5-
// TypeScript Version: 2.6
5+
// TypeScript Version: 2.8
66

77
/// <reference types="react" />
88

types/react-sortable-tree/index.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
// Jovica Zoric <https://github.com/jzoric>
55
// Kevin Perrine <https://github.com/kevinsperrine>
66
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
7-
// TypeScript Version: 2.6
7+
// TypeScript Version: 2.8
88

99
import * as React from 'react';
1010
import { ListProps, Index } from 'react-virtualized';

types/react-virtualized-select/index.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// Project: https://github.com/bvaughn/react-virtualized-select
33
// Definitions by: Sean Kelley <https://github.com/seansfkelley>
44
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
5-
// TypeScript Version: 2.6
5+
// TypeScript Version: 2.8
66

77
import * as React from "react";
88
import { ReactSelectProps, ReactAsyncSelectProps, ReactCreatableSelectProps, LoadOptionsHandler, OptionValues } from "react-select";

types/react-virtualized/index.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
// Steve Zhang <https://github.com/Stevearzh>
99
// Maciej Goszczycki <https://github.com/mgoszcz2>
1010
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
11-
// TypeScript Version: 2.6
11+
// TypeScript Version: 2.8
1212

1313
export {
1414
ArrowKeyStepper,

0 commit comments

Comments
 (0)