[prop-types] Do not consider null without undefined as optional (#38015)

* Do not consider null without undefined as optional

* Fix revealed errors in existing tests

- Non-required types can be undefined or null, so typescript types must reflect this

* Ensure undefined-ness is stored alongside nominalTypeHack

* Fix undefined in nominalTypeHack in a better way

* Add tests for nullable and undefinable types

* Fix formatting mistake

* Update downstream tests
This commit is contained in:
David Evans 2019-09-03 22:58:49 +01:00 committed by Ron Buckton
parent 2fa0eebeb7
commit dfe10fbcbe
3 changed files with 20 additions and 12 deletions

View File

@ -12,9 +12,9 @@ function FuncComp() {
return null;
}
// $ExpectType Requireable<number | null>
// $ExpectType Requireable<number | null | undefined>
AirbnbPropTypes.and([PropTypes.number]);
// $ExpectType Requireable<number | null>
// $ExpectType Requireable<number | null | undefined>
AirbnbPropTypes.and([PropTypes.number, AirbnbPropTypes.nonNegativeInteger]);
// $ExpectType Validator<number>
AirbnbPropTypes.and([PropTypes.number, AirbnbPropTypes.integer()], 'foo').isRequired;
@ -87,7 +87,7 @@ interface ForbidShape {
baz?: boolean | null;
}
// $ExpectType ValidationMap<{ foo: string | null; bar: number; baz: boolean | null; }>
// $ExpectType ValidationMap<{ foo: string | null | undefined; bar: number; baz: boolean | null | undefined; }>
AirbnbPropTypes.forbidExtraProps({
foo: PropTypes.string,
bar: PropTypes.number.isRequired,
@ -155,7 +155,7 @@ AirbnbPropTypes.range<5>(0, 10);
// $ExpectType Requireable<ReactLegacyRefLike<HTMLElement>>
AirbnbPropTypes.ref();
// $ExpectType Requireable<string | null>
// $ExpectType Requireable<string | null | undefined>
AirbnbPropTypes.requiredBy('foo', PropTypes.string);
// $ExpectType Validator<number>
AirbnbPropTypes.requiredBy('bar', PropTypes.number, 42).isRequired;
@ -177,11 +177,11 @@ interface ShapeShape {
bar?: number | null;
}
// $ExpectType Requireable<{ foo: string | null; }>
// $ExpectType Requireable<{ foo: string | null | undefined; }>
AirbnbPropTypes.shape({
foo: PropTypes.string,
});
// $ExpectType Requireable<{ foo: string | null; bar: number | null; }>
// $ExpectType Requireable<{ foo: string | null | undefined; bar: number | null | undefined; }>
AirbnbPropTypes.shape({
foo: PropTypes.string,
bar: PropTypes.number,
@ -200,5 +200,5 @@ AirbnbPropTypes.uniqueArray();
// $ExpectType Requireable<string[]>
AirbnbPropTypes.uniqueArray<string>();
// $ExpectType Requireable<{ [key: string]: number | null; }>
// $ExpectType Requireable<{ [key: string]: number | null | undefined; }>
AirbnbPropTypes.valuesOf(PropTypes.number);

View File

@ -31,7 +31,7 @@ export type ReactNodeLike =
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 IsOptional<T> = undefined extends T ? true : false;
export type RequiredKeys<V> = { [K in keyof V]-?: Exclude<V[K], undefined> extends Validator<infer T> ? IsOptional<T> extends true ? never : K : never }[keyof V];
export type OptionalKeys<V> = Exclude<keyof V, RequiredKeys<V>>;
@ -39,7 +39,9 @@ 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;
[nominalTypeHack]?: {
type: T;
};
}
export interface Requireable<T> extends Validator<T | undefined | null> {

View File

@ -20,7 +20,7 @@ interface Props {
instanceOf: TestClass;
oneOf: 'a' | 'b' | 'c';
oneOfType: string | boolean | {
foo?: string;
foo?: string | null;
bar: number;
};
numberOrFalse: false | number;
@ -29,10 +29,12 @@ interface Props {
objectOf: { [K: string]: number };
shape: {
foo: string;
bar?: boolean;
baz?: any
bar?: boolean | null;
baz?: any;
};
optionalNumber?: number | null;
nullableNumber: number | null;
undefinableNumber?: number;
customProp?: typeof uniqueType;
component: PropTypes.ReactComponentLike;
}
@ -75,6 +77,8 @@ const propTypes: PropTypesMap = {
objectOf: PropTypes.objectOf(PropTypes.number.isRequired).isRequired,
shape: PropTypes.shape(innerProps).isRequired,
optionalNumber: PropTypes.number,
nullableNumber: (() => null) as PropTypes.Validator<number | null>,
undefinableNumber: (() => null) as PropTypes.Validator<number | undefined>,
customProp: (() => null) as PropTypes.Validator<typeof uniqueType | undefined>,
component: PropTypes.elementType.isRequired
};
@ -102,6 +106,8 @@ const propTypesWithoutAnnotation = {
objectOf: PropTypes.objectOf(PropTypes.number.isRequired).isRequired,
shape: PropTypes.shape(innerProps).isRequired,
optionalNumber: PropTypes.number,
nullableNumber: (() => null) as PropTypes.Validator<number | null>,
undefinableNumber: (() => null) as PropTypes.Validator<number | undefined>,
customProp: (() => null) as PropTypes.Validator<typeof uniqueType | undefined>,
component: PropTypes.elementType.isRequired
};