[prop-types] use conditional types for better prop type inference (#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
This commit is contained in:
Ferdy Budhidharma 2018-07-21 07:41:15 -05:00 committed by John Reilly
parent ec4917b204
commit ffc8351127
15 changed files with 249 additions and 146 deletions

View File

@ -2,7 +2,7 @@
// Project: https://github.com/lo-tp/material-ui-pagination
// Definitions by: m0a <https://m0a.github.io>
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
// TypeScript Version: 2.6
// TypeScript Version: 2.8
import * as React from 'react';
export interface PaginationProps {
total: number;

View File

@ -14,7 +14,7 @@
// Sam Walsh <https://github.com/samwalshnz>
// Tim de Koning <https://github.com/reggino>
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
// TypeScript Version: 2.6
// TypeScript Version: 2.8
/// <reference types="react" />
/// <reference types="react-addons-linked-state-mixin" />

View File

@ -2,7 +2,7 @@
// Project: https://github.com/ngReact/ngReact
// Definitions by: Vicky Lai <https://github.com/velveret>
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
// TypeScript Version: 2.6
// TypeScript Version: 2.8
/// <reference types="angular"/>
/// <reference types="react"/>

View File

@ -1,33 +1,52 @@
// Type definitions for prop-types 15.5
// Project: https://github.com/reactjs/prop-types
// Definitions by: DovydasNavickas <https://github.com/DovydasNavickas>
// Ferdy Budhidharma <https://github.com/ferdaber>
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
// TypeScript Version: 2.2
// TypeScript Version: 2.8
export type Validator<T> = (object: T, key: string, componentName: string, ...rest: any[]) => Error | null;
import { ReactNode, ReactElement } from 'react';
export interface Requireable<T> extends Validator<T> {
isRequired: Validator<T>;
export const nominalTypeHack: unique symbol;
export type IsOptional<T> = undefined | null extends T ? true : undefined extends T ? true : null extends T ? true : false;
export type RequiredKeys<V> = { [K in keyof V]: V[K] extends Validator<infer T> ? IsOptional<T> extends true ? never : K : never }[keyof V];
export type OptionalKeys<V> = Exclude<keyof V, RequiredKeys<V>>;
export type InferPropsInner<V> = { [K in keyof V]: InferType<V[K]>; };
export interface Validator<T> {
(props: object, propName: string, componentName: string, location: string, propFullName: string): Error | null;
[nominalTypeHack]?: T;
}
export type ValidationMap<T> = {[K in keyof T]?: Validator<T> };
export interface Requireable<T> extends Validator<T | undefined | null> {
isRequired: Validator<NonNullable<T>>;
}
export type ValidationMap<T> = { [K in keyof T]-?: Validator<T[K]> };
export type InferType<V> = V extends Validator<infer T> ? T : any;
export type InferProps<V> =
& InferPropsInner<Pick<V, RequiredKeys<V>>>
& Partial<InferPropsInner<Pick<V, OptionalKeys<V>>>>;
export const any: Requireable<any>;
export const array: Requireable<any>;
export const bool: Requireable<any>;
export const func: Requireable<any>;
export const number: Requireable<any>;
export const object: Requireable<any>;
export const string: Requireable<any>;
export const node: Requireable<any>;
export const element: Requireable<any>;
export const symbol: Requireable<any>;
export function instanceOf(expectedClass: {}): Requireable<any>;
export function oneOf(types: any[]): Requireable<any>;
export function oneOfType(types: Array<Validator<any>>): Requireable<any>;
export function arrayOf(type: Validator<any>): Requireable<any>;
export function objectOf(type: Validator<any>): Requireable<any>;
export function shape(type: ValidationMap<any>): Requireable<any>;
export const array: Requireable<any[]>;
export const bool: Requireable<boolean>;
export const func: Requireable<(...args: any[]) => any>;
export const number: Requireable<number>;
export const object: Requireable<object>;
export const string: Requireable<string>;
export const node: Requireable<ReactNode>;
export const element: Requireable<ReactElement<any>>;
export const symbol: Requireable<symbol>;
export function instanceOf<T>(expectedClass: new (...args: any[]) => T): Requireable<T>;
export function oneOf<T>(types: T[]): Requireable<T>;
export function oneOfType<T extends Validator<any>>(types: T[]): Requireable<NonNullable<InferType<T>>>;
export function arrayOf<T>(type: Validator<T>): Requireable<T[]>;
export function objectOf<T>(type: Validator<T>): Requireable<{ [K in keyof any]: T; }>;
export function shape<P extends ValidationMap<any>>(type: P): Requireable<InferProps<P>>;
/**
* Assert that the values match with the type specs.

View File

@ -1,29 +1,207 @@
import { ReactElement, ReactNode } from "react";
import * as PropTypes from "prop-types";
declare const uniqueType: unique symbol;
class TestClass { }
interface Props {
any: any;
any?: any;
array: string[];
bool: boolean;
func: any;
string: string;
element: ReactElement<any>;
func(foo: string): void;
node?: ReactNode;
requiredNode: NonNullable<ReactNode>;
number: number;
object: object;
string: string;
symbol: symbol;
object: {};
node: any;
element: any;
instanceOf: TestClass;
oneOf: 'a' | 'b' | 'c';
oneOfType: string | boolean | {
foo?: string;
bar: number;
};
numberOrFalse: false | number;
nodeOrRenderFn?: ReactNode | (() => ReactNode);
arrayOf: boolean[];
objectOf: { [K: string]: number };
shape: {
foo: string;
bar?: boolean;
baz?: any
};
optionalNumber?: number | null;
customProp?: typeof uniqueType;
}
const propTypes: PropTypes.ValidationMap<Props> = {
any: PropTypes.any.isRequired,
const innerProps = {
foo: PropTypes.string.isRequired,
bar: PropTypes.bool,
baz: PropTypes.any
};
const arrayOfTypes = [PropTypes.string, PropTypes.bool, PropTypes.shape({
foo: PropTypes.string,
bar: PropTypes.number.isRequired
})];
type PropTypesMap = PropTypes.ValidationMap<Props>;
// TS checking
const propTypes: PropTypesMap = {
any: PropTypes.any,
array: PropTypes.array.isRequired,
bool: PropTypes.bool.isRequired,
element: PropTypes.element.isRequired,
func: PropTypes.func.isRequired,
node: PropTypes.node,
requiredNode: PropTypes.node.isRequired,
number: PropTypes.number.isRequired,
object: PropTypes.object.isRequired,
string: PropTypes.string.isRequired,
symbol: PropTypes.symbol.isRequired,
node: PropTypes.node.isRequired,
element: PropTypes.element.isRequired
instanceOf: PropTypes.instanceOf(TestClass).isRequired,
oneOf: PropTypes.oneOf<'a' | 'b' | 'c'>(['a', 'b', 'c']).isRequired,
oneOfType: PropTypes.oneOfType(arrayOfTypes).isRequired,
numberOrFalse: PropTypes.oneOfType([PropTypes.oneOf<false>([false]), PropTypes.number]).isRequired,
// The generic function type (() => any) is assignable to ReactNode because ReactNode extends the empty object type {}
// Which widens the array literal of validators to just Array<Requireable<() => any>>
// 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
// So we assert the explicit tuple type
nodeOrRenderFn: PropTypes.oneOfType([PropTypes.node, PropTypes.func] as [PropTypes.Requireable<ReactNode>, PropTypes.Requireable<() => any>]),
arrayOf: PropTypes.arrayOf(PropTypes.bool.isRequired).isRequired,
objectOf: PropTypes.objectOf(PropTypes.number.isRequired).isRequired,
shape: PropTypes.shape(innerProps).isRequired,
optionalNumber: PropTypes.number,
customProp: (() => null) as PropTypes.Validator<typeof uniqueType | undefined>
};
PropTypes.checkPropTypes({xs: PropTypes.array}, {xs: []}, 'location', 'componentName');
// JS checking
const propTypesWithoutAnnotation = {
any: PropTypes.any,
array: PropTypes.array.isRequired,
bool: PropTypes.bool.isRequired,
element: PropTypes.element.isRequired,
func: PropTypes.func.isRequired,
node: PropTypes.node,
requiredNode: PropTypes.node.isRequired,
number: PropTypes.number.isRequired,
object: PropTypes.object.isRequired,
string: PropTypes.string.isRequired,
symbol: PropTypes.symbol.isRequired,
instanceOf: PropTypes.instanceOf(TestClass).isRequired,
// required generic specification because of array type widening
oneOf: PropTypes.oneOf<'a' | 'b' | 'c'>(['a', 'b', 'c']).isRequired,
oneOfType: PropTypes.oneOfType(arrayOfTypes).isRequired,
numberOrFalse: PropTypes.oneOfType([PropTypes.oneOf<false>([false]), PropTypes.number]).isRequired,
nodeOrRenderFn: PropTypes.oneOfType([PropTypes.node, PropTypes.func] as [PropTypes.Requireable<ReactNode>, PropTypes.Requireable<() => any>]),
arrayOf: PropTypes.arrayOf(PropTypes.bool.isRequired).isRequired,
objectOf: PropTypes.objectOf(PropTypes.number.isRequired).isRequired,
shape: PropTypes.shape(innerProps).isRequired,
optionalNumber: PropTypes.number,
customProp: (() => null) as PropTypes.Validator<typeof uniqueType | undefined>
};
const partialPropTypes = {
number: PropTypes.number.isRequired,
object: PropTypes.object.isRequired,
string: PropTypes.string.isRequired,
symbol: PropTypes.symbol.isRequired,
};
const outerPropTypes = {
props: PropTypes.shape(propTypes).isRequired
};
const outerPropTypesWithoutAnnotation = {
props: PropTypes.shape(propTypesWithoutAnnotation).isRequired
};
type ExtractedArrayProps = PropTypes.InferType<(typeof arrayOfTypes)[number]>;
type ExtractedInnerProps = PropTypes.InferProps<typeof innerProps>;
type ExtractedProps = PropTypes.InferProps<typeof propTypes>;
type ExtractedPropsFromOuterProps = PropTypes.InferProps<typeof outerPropTypes>['props'];
type ExtractedPartialProps = PropTypes.InferProps<typeof partialPropTypes>;
type ExtractedPropsWithoutAnnotation = PropTypes.InferProps<typeof propTypesWithoutAnnotation>;
type ExtractedPropsFromOuterPropsWithoutAnnotation = PropTypes.InferProps<typeof outerPropTypesWithoutAnnotation>['props'];
// $ExpectType: true
type ExtractPropsMatch = ExtractedProps extends ExtractedPropsWithoutAnnotation ? true : false;
// $ExpectType: true
type ExtractPropsMatch2 = ExtractedPropsWithoutAnnotation extends ExtractedProps ? true : false;
// $ExpectType: true
type ExtractPropsMatch3 = ExtractedProps extends Props ? true : false;
// $ExpectType: true
type ExtractPropsMatch4 = Props extends ExtractedPropsWithoutAnnotation ? true : false;
// $ExpectType: true
type ExtractFromOuterPropsMatch = ExtractedPropsFromOuterProps extends ExtractedPropsFromOuterPropsWithoutAnnotation ? true : false;
// $ExpectType: true
type ExtractFromOuterPropsMatch2 = ExtractedPropsFromOuterPropsWithoutAnnotation extends ExtractedPropsFromOuterProps ? true : false;
// $ExpectType: true
type ExtractFromOuterPropsMatch3 = ExtractedPropsFromOuterProps extends Props ? true : false;
// $ExpectType: true
type ExtractFromOuterPropsMatch4 = Props extends ExtractedPropsFromOuterPropsWithoutAnnotation ? true : false;
// $ExpectType: false
type ExtractPropsMismatch = ExtractedPartialProps extends Props ? true : false;
// $ExpectType: {}
type UnmatchedPropKeys = Pick<ExtractedPropsWithoutAnnotation, Extract<{
[K in keyof ExtractedPropsWithoutAnnotation]: ExtractedPropsWithoutAnnotation[K] extends ExtractedProps[K] ? never : K
}[keyof ExtractedPropsWithoutAnnotation], keyof ExtractedPropsWithoutAnnotation>>;
// $ExpectType: {}
type UnmatchedPropKeys2 = Pick<ExtractedProps, Extract<{
[K in keyof ExtractedProps]: ExtractedProps[K] extends ExtractedPropsWithoutAnnotation[K] ? never : K
}[keyof ExtractedProps], keyof ExtractedProps>>;
PropTypes.checkPropTypes({ xs: PropTypes.array }, { xs: [] }, 'location', 'componentName');
// This would be the type that JSX sees
type Defaultize<T, D> =
& Pick<T, Exclude<keyof T, keyof D>>
& Partial<Pick<T, Extract<keyof T, keyof D>>>
& Partial<Pick<D, Exclude<keyof D, keyof T>>>;
// This would be the type inside the component
type Undefaultize<T, D> =
& Pick<T, Exclude<keyof T, keyof D>>
& { [K in Extract<keyof T, keyof D>]-?: Exclude<T[K], undefined>; }
& Required<Pick<D, Exclude<keyof D, keyof T>>>;
const componentPropTypes = {
fi: PropTypes.func.isRequired,
foo: PropTypes.string,
bar: PropTypes.number.isRequired,
baz: PropTypes.bool,
bat: PropTypes.node
};
const componentDefaultProps = {
fi: () => null,
baz: false,
bat: ['This', 'is', 'a', 'string']
};
type DefaultizedProps = Defaultize<PropTypes.InferProps<typeof componentPropTypes>, typeof componentDefaultProps>;
type UndefaultizedProps = Undefaultize<PropTypes.InferProps<typeof componentPropTypes>, typeof componentDefaultProps>;
// $ExpectType: true
type DefaultizedPropsTest = {
fi?: (...args: any[]) => any;
foo?: string | null;
bar: number;
baz?: boolean | null;
bat?: ReactNode;
} extends DefaultizedProps ? true : false;
// $ExpectType: true
type UndefaultizedPropsTest = {
fi: (...args: any[]) => any;
foo?: string | null;
bar: number;
baz: boolean;
bat: Exclude<ReactNode, undefined>;
} extends UndefaultizedProps ? true : false;

View File

@ -2,7 +2,7 @@
// Project: https://github.com/digiaonline/react-foundation
// Definitions by: Daniel Earwicker <https://github.com/danielearwicker>
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
// TypeScript Version: 2.6
// TypeScript Version: 2.8
export { Accordion, AccordionItem, AccordionTitle, AccordionContent } from './components/accordion';
export { Badge } from './components/badge';

View File

@ -2,7 +2,7 @@
// Project: https://github.com/Aweary/react-is-deprecated
// Definitions by: Sean Kelley <https://github.com/seansfkelley>
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
// TypeScript Version: 2.6
// TypeScript Version: 2.8
declare module 'react-is-deprecated' {
import { Validator, Requireable, ValidationMap, ReactPropTypes } from 'react';

View File

@ -2,7 +2,7 @@
// Project: https://github.com/PaulLeCam/react-leaflet
// Definitions by: Dave Leaver <https://github.com/danzel>, David Schneider <https://github.com/davschne>, Yui T. <https://github.com/yuit>
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
// TypeScript Version: 2.6
// TypeScript Version: 2.8
import * as Leaflet from 'leaflet';
import * as React from 'react';

View File

@ -2,7 +2,7 @@
// Project: https://github.com/popkirby/react-props-decorators
// Definitions by: Qubo <https://github.com/tkqubo>
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
// TypeScript Version: 2.6
// TypeScript Version: 2.8
/// <reference types="react" />

View File

@ -4,7 +4,7 @@
// Jovica Zoric <https://github.com/jzoric>
// Kevin Perrine <https://github.com/kevinsperrine>
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
// TypeScript Version: 2.6
// TypeScript Version: 2.8
import * as React from 'react';
import { ListProps, Index } from 'react-virtualized';

View File

@ -2,7 +2,7 @@
// Project: https://github.com/bvaughn/react-virtualized-select
// Definitions by: Sean Kelley <https://github.com/seansfkelley>
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
// TypeScript Version: 2.6
// TypeScript Version: 2.8
import * as React from "react";
import { ReactSelectProps, ReactAsyncSelectProps, ReactCreatableSelectProps, LoadOptionsHandler, OptionValues } from "react-select";

View File

@ -8,7 +8,7 @@
// Steve Zhang <https://github.com/Stevearzh>
// Maciej Goszczycki <https://github.com/mgoszcz2>
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
// TypeScript Version: 2.6
// TypeScript Version: 2.8
export {
ArrowKeyStepper,

View File

@ -11,7 +11,6 @@ import * as TestUtils from "react-addons-test-utils";
import TransitionGroup = require("react-addons-transition-group");
import update = require("react-addons-update");
import createReactClass = require("create-react-class");
import * as PropTypes from "prop-types";
import * as DOM from "react-dom-factories";
interface Props extends React.Attributes {
@ -91,10 +90,10 @@ declare const container: Element;
};
// Even if state is not set, this is allowed by React
this.setState({inputValue: 'hello'});
this.setState({ inputValue: 'hello' });
this.setState((prevState, props) => {
// $ExpectError
props = {foo: 'nope'};
props = { foo: 'nope' };
// $ExpectError
props.foo = 'nope';
@ -118,15 +117,15 @@ declare const container: Element;
class ModernComponent extends React.Component<Props, State, Snapshot>
implements MyComponent, React.ChildContextProvider<ChildContext> {
static propTypes: React.ValidationMap<Props> = {
foo: PropTypes.number
foo: (() => null) as React.Validator<Props>
};
static contextTypes: React.ValidationMap<Context> = {
someValue: PropTypes.string
someValue: (() => null) as React.Validator<Context>
};
static childContextTypes: React.ValidationMap<ChildContext> = {
someOtherValue: PropTypes.string
someOtherValue: (() => null) as React.Validator<ChildContext>
};
context: Context;
@ -180,7 +179,7 @@ class ModernComponent extends React.Component<Props, State, Snapshot>
class ModernComponentArrayRender extends React.Component<Props> {
render() {
return [DOM.h1({ key: "1" }, "1"),
DOM.h1({ key: "2" }, "2")];
DOM.h1({ key: "2" }, "2")];
}
}
@ -436,99 +435,6 @@ DOM.svg({
})
);
//
// PropTypes
// --------------------------------------------------------------------------
const PropTypesSpecification: React.ComponentSpec<any, any> = {
propTypes: {
optionalArray: PropTypes.array,
optionalBool: PropTypes.bool,
optionalFunc: PropTypes.func,
optionalNumber: PropTypes.number,
optionalObject: PropTypes.object,
optionalString: PropTypes.string,
optionalNode: PropTypes.node,
optionalElement: PropTypes.element,
optionalMessage: PropTypes.instanceOf(Date),
optionalEnum: PropTypes.oneOf(["News", "Photos"]),
optionalUnion: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
PropTypes.instanceOf(Date)
]),
optionalArrayOf: PropTypes.arrayOf(PropTypes.number),
optionalObjectOf: PropTypes.objectOf(PropTypes.number),
optionalObjectWithShape: PropTypes.shape({
color: PropTypes.string,
fontSize: PropTypes.number
}),
requiredFunc: PropTypes.func.isRequired,
requiredAny: PropTypes.any.isRequired,
customProp(props: any, propName: string, componentName: string): Error | null {
if (!/matchme/.test(props[propName])) {
return new Error("Validation failed!");
}
return null;
},
// https://facebook.github.io/react/warnings/dont-call-proptypes.html#fixing-the-false-positive-in-third-party-proptypes
percentage: (object: any, key: string, componentName: string, ...rest: any[]): Error | null => {
const error = PropTypes.number(object, key, componentName, ...rest);
if (error) {
return error;
}
if (object[key] < 0 || object[key] > 100) {
return new Error(`prop ${key} must be between 0 and 100`);
}
return null;
}
},
render: (): React.ReactElement<any> | null => {
return null;
}
};
//
// ContextTypes
// --------------------------------------------------------------------------
const ContextTypesSpecification: React.ComponentSpec<any, any> = {
contextTypes: {
optionalArray: PropTypes.array,
optionalBool: PropTypes.bool,
optionalFunc: PropTypes.func,
optionalNumber: PropTypes.number,
optionalObject: PropTypes.object,
optionalString: PropTypes.string,
optionalNode: PropTypes.node,
optionalElement: PropTypes.element,
optionalMessage: PropTypes.instanceOf(Date),
optionalEnum: PropTypes.oneOf(["News", "Photos"]),
optionalUnion: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
PropTypes.instanceOf(Date)
]),
optionalArrayOf: PropTypes.arrayOf(PropTypes.number),
optionalObjectOf: PropTypes.objectOf(PropTypes.number),
optionalObjectWithShape: PropTypes.shape({
color: PropTypes.string,
fontSize: PropTypes.number
}),
requiredFunc: PropTypes.func.isRequired,
requiredAny: PropTypes.any.isRequired,
customProp(props: any, propName: string, componentName: string): Error | null {
if (!/matchme/.test(props[propName])) {
return new Error("Validation failed!");
}
return null;
}
},
render: (): null => {
return null;
}
};
//
// React.Children
// --------------------------------------------------------------------------

View File

@ -10,7 +10,7 @@
// Tim de Koning <https://github.com/reggino>
// Maddi Joyce <https://github.com/maddijoyce>
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
// TypeScript Version: 2.6
// TypeScript Version: 2.8
import {
ComponentClass,
StatelessComponent,

View File

@ -2,12 +2,12 @@
// Project: https://github.com/erikras/redux-form
// Definitions by: Daniel Lytkin <https://github.com/aikoven>
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
// TypeScript Version: 2.6
// TypeScript Version: 2.8
import * as React from 'react';
import { Dispatch, ActionCreator, Reducer } from 'redux';
export const actionTypes: {[actionName: string]: string};
export const actionTypes: { [actionName: string]: string };
export type FieldValue = any;
@ -464,7 +464,7 @@ export declare const reducer: {
[formName: string]: {
[fieldName: string]: Normalizer
}
}): Reducer<any>;
}): Reducer;
/**
* Returns a form reducer that will also pass each action through
@ -473,5 +473,5 @@ export declare const reducer: {
* passed to each reducer will only be the slice that pertains to that
* form.
*/
plugin(reducers: { [formName: string]: Reducer<any> }): Reducer<any>;
plugin(reducers: { [formName: string]: Reducer }): Reducer;
};