mirror of
https://github.com/microsoft/TypeScript.git
synced 2026-05-13 23:48:54 -05:00
Discriminate jsx contextual types same as object contextual types (#27408)
* Discriminate jsx contextual types same as object contextual types * Extract core discrimination algorithm to getDiscriminationResultForProperty * Merge all discrimination implementations * Fix lints
This commit is contained in:
@@ -11640,27 +11640,14 @@ namespace ts {
|
||||
|
||||
// Keep this up-to-date with the same logic within `getApparentTypeOfContextualType`, since they should behave similarly
|
||||
function findMatchingDiscriminantType(source: Type, target: UnionOrIntersectionType) {
|
||||
let match: Type | undefined;
|
||||
const sourceProperties = getPropertiesOfObjectType(source);
|
||||
if (sourceProperties) {
|
||||
const sourcePropertiesFiltered = findDiscriminantProperties(sourceProperties, target);
|
||||
if (sourcePropertiesFiltered) {
|
||||
for (const sourceProperty of sourcePropertiesFiltered) {
|
||||
const sourceType = getTypeOfSymbol(sourceProperty);
|
||||
for (const type of target.types) {
|
||||
const targetType = getTypeOfPropertyOfType(type, sourceProperty.escapedName);
|
||||
if (targetType && isRelatedTo(sourceType, targetType)) {
|
||||
if (type === match) continue; // Finding multiple fields which discriminate to the same type is fine
|
||||
if (match) {
|
||||
return undefined;
|
||||
}
|
||||
match = type;
|
||||
}
|
||||
}
|
||||
}
|
||||
return discriminateTypeByDiscriminableItems(target, map(sourcePropertiesFiltered, p => ([() => getTypeOfSymbol(p), p.escapedName] as [() => Type, __String])), isRelatedTo);
|
||||
}
|
||||
}
|
||||
return match;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function typeRelatedToEachType(source: Type, target: IntersectionType, reportErrors: boolean): Ternary {
|
||||
@@ -12475,6 +12462,25 @@ namespace ts {
|
||||
}
|
||||
}
|
||||
|
||||
function discriminateTypeByDiscriminableItems(target: UnionType, discriminators: [() => Type, __String][], related: (source: Type, target: Type) => boolean | Ternary): Type | undefined;
|
||||
function discriminateTypeByDiscriminableItems(target: UnionType, discriminators: [() => Type, __String][], related: (source: Type, target: Type) => boolean | Ternary, defaultValue: Type): Type;
|
||||
function discriminateTypeByDiscriminableItems(target: UnionType, discriminators: [() => Type, __String][], related: (source: Type, target: Type) => boolean | Ternary, defaultValue?: Type) {
|
||||
let match: Type | undefined;
|
||||
for (const [getDiscriminatingType, propertyName] of discriminators) {
|
||||
for (const type of target.types) {
|
||||
const targetType = getTypeOfPropertyOfType(type, propertyName);
|
||||
if (targetType && related(getDiscriminatingType(), targetType)) {
|
||||
if (match) {
|
||||
if (type === match) continue; // Finding multiple fields which discriminate to the same type is fine
|
||||
return defaultValue;
|
||||
}
|
||||
match = type;
|
||||
}
|
||||
}
|
||||
}
|
||||
return match || defaultValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* A type is 'weak' if it is an object type with at least one optional property
|
||||
* and no required properties, call/construct signatures or index signatures
|
||||
@@ -14187,7 +14193,7 @@ namespace ts {
|
||||
if ((<TransientSymbol>prop).isDiscriminantProperty === undefined) {
|
||||
(<TransientSymbol>prop).isDiscriminantProperty = !!((<TransientSymbol>prop).checkFlags & CheckFlags.HasNonUniformType) && isLiteralType(getTypeOfSymbol(prop));
|
||||
}
|
||||
return (<TransientSymbol>prop).isDiscriminantProperty;
|
||||
return !!(<TransientSymbol>prop).isDiscriminantProperty;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
@@ -16762,43 +16768,53 @@ namespace ts {
|
||||
case SyntaxKind.FalseKeyword:
|
||||
case SyntaxKind.NullKeyword:
|
||||
case SyntaxKind.Identifier:
|
||||
case SyntaxKind.UndefinedKeyword:
|
||||
return true;
|
||||
case SyntaxKind.PropertyAccessExpression:
|
||||
case SyntaxKind.ParenthesizedExpression:
|
||||
return isPossiblyDiscriminantValue((<PropertyAccessExpression | ParenthesizedExpression>node).expression);
|
||||
case SyntaxKind.JsxExpression:
|
||||
return !(node as JsxExpression).expression || isPossiblyDiscriminantValue((node as JsxExpression).expression!);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function discriminateContextualTypeByObjectMembers(node: ObjectLiteralExpression, contextualType: UnionType) {
|
||||
return discriminateTypeByDiscriminableItems(contextualType,
|
||||
map(
|
||||
filter(node.properties, p => !!p.symbol && p.kind === SyntaxKind.PropertyAssignment && isPossiblyDiscriminantValue(p.initializer) && isDiscriminantProperty(contextualType, p.symbol.escapedName)),
|
||||
prop => ([() => checkExpression((prop as PropertyAssignment).initializer), prop.symbol.escapedName] as [() => Type, __String])
|
||||
),
|
||||
isTypeAssignableTo,
|
||||
contextualType
|
||||
);
|
||||
}
|
||||
|
||||
function discriminateContextualTypeByJSXAttributes(node: JsxAttributes, contextualType: UnionType) {
|
||||
return discriminateTypeByDiscriminableItems(contextualType,
|
||||
map(
|
||||
filter(node.properties, p => !!p.symbol && p.kind === SyntaxKind.JsxAttribute && isDiscriminantProperty(contextualType, p.symbol.escapedName) && (!p.initializer || isPossiblyDiscriminantValue(p.initializer))),
|
||||
prop => ([!(prop as JsxAttribute).initializer ? (() => trueType) : (() => checkExpression((prop as JsxAttribute).initializer!)), prop.symbol.escapedName] as [() => Type, __String])
|
||||
),
|
||||
isTypeAssignableTo,
|
||||
contextualType
|
||||
);
|
||||
}
|
||||
|
||||
// Return the contextual type for a given expression node. During overload resolution, a contextual type may temporarily
|
||||
// be "pushed" onto a node using the contextualType property.
|
||||
function getApparentTypeOfContextualType(node: Expression): Type | undefined {
|
||||
let contextualType = getContextualType(node);
|
||||
contextualType = contextualType && mapType(contextualType, getApparentType);
|
||||
if (!(contextualType && contextualType.flags & TypeFlags.Union && isObjectLiteralExpression(node))) {
|
||||
return contextualType;
|
||||
}
|
||||
// Keep the below up-to-date with the work done within `isRelatedTo` by `findMatchingDiscriminantType`
|
||||
let match: Type | undefined;
|
||||
propLoop: for (const prop of node.properties) {
|
||||
if (!prop.symbol) continue;
|
||||
if (prop.kind !== SyntaxKind.PropertyAssignment) continue;
|
||||
if (isPossiblyDiscriminantValue(prop.initializer) && isDiscriminantProperty(contextualType, prop.symbol.escapedName)) {
|
||||
const discriminatingType = checkExpression(prop.initializer);
|
||||
for (const type of (contextualType as UnionType).types) {
|
||||
const targetType = getTypeOfPropertyOfType(type, prop.symbol.escapedName);
|
||||
if (targetType && isTypeAssignableTo(discriminatingType, targetType)) {
|
||||
if (match) {
|
||||
if (type === match) continue; // Finding multiple fields which discriminate to the same type is fine
|
||||
match = undefined;
|
||||
break propLoop;
|
||||
}
|
||||
match = type;
|
||||
}
|
||||
}
|
||||
if (contextualType && contextualType.flags & TypeFlags.Union) {
|
||||
if (isObjectLiteralExpression(node)) {
|
||||
return discriminateContextualTypeByObjectMembers(node, contextualType as UnionType);
|
||||
}
|
||||
else if (isJsxAttributes(node)) {
|
||||
return discriminateContextualTypeByJSXAttributes(node, contextualType as UnionType);
|
||||
}
|
||||
}
|
||||
return match || contextualType;
|
||||
return contextualType;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
//// [checkJsxUnionSFXContextualTypeInferredCorrectly.tsx]
|
||||
/// <reference path="/.lib/react16.d.ts" />
|
||||
|
||||
import React from 'react';
|
||||
|
||||
interface PS {
|
||||
multi: false
|
||||
value: string | undefined
|
||||
onChange: (selection: string | undefined) => void
|
||||
}
|
||||
|
||||
interface PM {
|
||||
multi: true
|
||||
value: string[]
|
||||
onChange: (selection: string[]) => void
|
||||
}
|
||||
|
||||
export function ComponentWithUnion(props: PM | PS) {
|
||||
return <h1></h1>;
|
||||
}
|
||||
|
||||
// Usage with React tsx
|
||||
export function HereIsTheError() {
|
||||
return (
|
||||
<ComponentWithUnion
|
||||
multi={false}
|
||||
value={'s'}
|
||||
onChange={val => console.log(val)} // <- this throws an error
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// Usage with pure TypeScript
|
||||
ComponentWithUnion({
|
||||
multi: false,
|
||||
value: 's',
|
||||
onChange: val => console.log(val) // <- this works fine
|
||||
});
|
||||
|
||||
|
||||
//// [checkJsxUnionSFXContextualTypeInferredCorrectly.js]
|
||||
"use strict";
|
||||
/// <reference path="react16.d.ts" />
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
exports.__esModule = true;
|
||||
var react_1 = __importDefault(require("react"));
|
||||
function ComponentWithUnion(props) {
|
||||
return react_1["default"].createElement("h1", null);
|
||||
}
|
||||
exports.ComponentWithUnion = ComponentWithUnion;
|
||||
// Usage with React tsx
|
||||
function HereIsTheError() {
|
||||
return (react_1["default"].createElement(ComponentWithUnion, { multi: false, value: 's', onChange: function (val) { return console.log(val); } }));
|
||||
}
|
||||
exports.HereIsTheError = HereIsTheError;
|
||||
// Usage with pure TypeScript
|
||||
ComponentWithUnion({
|
||||
multi: false,
|
||||
value: 's',
|
||||
onChange: function (val) { return console.log(val); } // <- this works fine
|
||||
});
|
||||
@@ -0,0 +1,91 @@
|
||||
=== tests/cases/conformance/jsx/checkJsxUnionSFXContextualTypeInferredCorrectly.tsx ===
|
||||
/// <reference path="react16.d.ts" />
|
||||
|
||||
import React from 'react';
|
||||
>React : Symbol(React, Decl(checkJsxUnionSFXContextualTypeInferredCorrectly.tsx, 2, 6))
|
||||
|
||||
interface PS {
|
||||
>PS : Symbol(PS, Decl(checkJsxUnionSFXContextualTypeInferredCorrectly.tsx, 2, 26))
|
||||
|
||||
multi: false
|
||||
>multi : Symbol(PS.multi, Decl(checkJsxUnionSFXContextualTypeInferredCorrectly.tsx, 4, 14))
|
||||
|
||||
value: string | undefined
|
||||
>value : Symbol(PS.value, Decl(checkJsxUnionSFXContextualTypeInferredCorrectly.tsx, 5, 16))
|
||||
|
||||
onChange: (selection: string | undefined) => void
|
||||
>onChange : Symbol(PS.onChange, Decl(checkJsxUnionSFXContextualTypeInferredCorrectly.tsx, 6, 29))
|
||||
>selection : Symbol(selection, Decl(checkJsxUnionSFXContextualTypeInferredCorrectly.tsx, 7, 15))
|
||||
}
|
||||
|
||||
interface PM {
|
||||
>PM : Symbol(PM, Decl(checkJsxUnionSFXContextualTypeInferredCorrectly.tsx, 8, 1))
|
||||
|
||||
multi: true
|
||||
>multi : Symbol(PM.multi, Decl(checkJsxUnionSFXContextualTypeInferredCorrectly.tsx, 10, 14))
|
||||
|
||||
value: string[]
|
||||
>value : Symbol(PM.value, Decl(checkJsxUnionSFXContextualTypeInferredCorrectly.tsx, 11, 15))
|
||||
|
||||
onChange: (selection: string[]) => void
|
||||
>onChange : Symbol(PM.onChange, Decl(checkJsxUnionSFXContextualTypeInferredCorrectly.tsx, 12, 19))
|
||||
>selection : Symbol(selection, Decl(checkJsxUnionSFXContextualTypeInferredCorrectly.tsx, 13, 15))
|
||||
}
|
||||
|
||||
export function ComponentWithUnion(props: PM | PS) {
|
||||
>ComponentWithUnion : Symbol(ComponentWithUnion, Decl(checkJsxUnionSFXContextualTypeInferredCorrectly.tsx, 14, 1))
|
||||
>props : Symbol(props, Decl(checkJsxUnionSFXContextualTypeInferredCorrectly.tsx, 16, 35))
|
||||
>PM : Symbol(PM, Decl(checkJsxUnionSFXContextualTypeInferredCorrectly.tsx, 8, 1))
|
||||
>PS : Symbol(PS, Decl(checkJsxUnionSFXContextualTypeInferredCorrectly.tsx, 2, 26))
|
||||
|
||||
return <h1></h1>;
|
||||
>h1 : Symbol(JSX.IntrinsicElements.h1, Decl(react16.d.ts, 2430, 106))
|
||||
>h1 : Symbol(JSX.IntrinsicElements.h1, Decl(react16.d.ts, 2430, 106))
|
||||
}
|
||||
|
||||
// Usage with React tsx
|
||||
export function HereIsTheError() {
|
||||
>HereIsTheError : Symbol(HereIsTheError, Decl(checkJsxUnionSFXContextualTypeInferredCorrectly.tsx, 18, 1))
|
||||
|
||||
return (
|
||||
<ComponentWithUnion
|
||||
>ComponentWithUnion : Symbol(ComponentWithUnion, Decl(checkJsxUnionSFXContextualTypeInferredCorrectly.tsx, 14, 1))
|
||||
|
||||
multi={false}
|
||||
>multi : Symbol(multi, Decl(checkJsxUnionSFXContextualTypeInferredCorrectly.tsx, 23, 27))
|
||||
|
||||
value={'s'}
|
||||
>value : Symbol(value, Decl(checkJsxUnionSFXContextualTypeInferredCorrectly.tsx, 24, 25))
|
||||
|
||||
onChange={val => console.log(val)} // <- this throws an error
|
||||
>onChange : Symbol(onChange, Decl(checkJsxUnionSFXContextualTypeInferredCorrectly.tsx, 25, 23))
|
||||
>val : Symbol(val, Decl(checkJsxUnionSFXContextualTypeInferredCorrectly.tsx, 26, 22))
|
||||
>console.log : Symbol(Console.log, Decl(lib.dom.d.ts, --, --))
|
||||
>console : Symbol(console, Decl(lib.dom.d.ts, --, --))
|
||||
>log : Symbol(Console.log, Decl(lib.dom.d.ts, --, --))
|
||||
>val : Symbol(val, Decl(checkJsxUnionSFXContextualTypeInferredCorrectly.tsx, 26, 22))
|
||||
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// Usage with pure TypeScript
|
||||
ComponentWithUnion({
|
||||
>ComponentWithUnion : Symbol(ComponentWithUnion, Decl(checkJsxUnionSFXContextualTypeInferredCorrectly.tsx, 14, 1))
|
||||
|
||||
multi: false,
|
||||
>multi : Symbol(multi, Decl(checkJsxUnionSFXContextualTypeInferredCorrectly.tsx, 32, 20))
|
||||
|
||||
value: 's',
|
||||
>value : Symbol(value, Decl(checkJsxUnionSFXContextualTypeInferredCorrectly.tsx, 33, 17))
|
||||
|
||||
onChange: val => console.log(val) // <- this works fine
|
||||
>onChange : Symbol(onChange, Decl(checkJsxUnionSFXContextualTypeInferredCorrectly.tsx, 34, 15))
|
||||
>val : Symbol(val, Decl(checkJsxUnionSFXContextualTypeInferredCorrectly.tsx, 35, 13))
|
||||
>console.log : Symbol(Console.log, Decl(lib.dom.d.ts, --, --))
|
||||
>console : Symbol(console, Decl(lib.dom.d.ts, --, --))
|
||||
>log : Symbol(Console.log, Decl(lib.dom.d.ts, --, --))
|
||||
>val : Symbol(val, Decl(checkJsxUnionSFXContextualTypeInferredCorrectly.tsx, 35, 13))
|
||||
|
||||
});
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
=== tests/cases/conformance/jsx/checkJsxUnionSFXContextualTypeInferredCorrectly.tsx ===
|
||||
/// <reference path="react16.d.ts" />
|
||||
|
||||
import React from 'react';
|
||||
>React : typeof React
|
||||
|
||||
interface PS {
|
||||
multi: false
|
||||
>multi : false
|
||||
>false : false
|
||||
|
||||
value: string | undefined
|
||||
>value : string | undefined
|
||||
|
||||
onChange: (selection: string | undefined) => void
|
||||
>onChange : (selection: string | undefined) => void
|
||||
>selection : string | undefined
|
||||
}
|
||||
|
||||
interface PM {
|
||||
multi: true
|
||||
>multi : true
|
||||
>true : true
|
||||
|
||||
value: string[]
|
||||
>value : string[]
|
||||
|
||||
onChange: (selection: string[]) => void
|
||||
>onChange : (selection: string[]) => void
|
||||
>selection : string[]
|
||||
}
|
||||
|
||||
export function ComponentWithUnion(props: PM | PS) {
|
||||
>ComponentWithUnion : (props: PS | PM) => JSX.Element
|
||||
>props : PS | PM
|
||||
|
||||
return <h1></h1>;
|
||||
><h1></h1> : JSX.Element
|
||||
>h1 : any
|
||||
>h1 : any
|
||||
}
|
||||
|
||||
// Usage with React tsx
|
||||
export function HereIsTheError() {
|
||||
>HereIsTheError : () => JSX.Element
|
||||
|
||||
return (
|
||||
>( <ComponentWithUnion multi={false} value={'s'} onChange={val => console.log(val)} // <- this throws an error /> ) : JSX.Element
|
||||
|
||||
<ComponentWithUnion
|
||||
><ComponentWithUnion multi={false} value={'s'} onChange={val => console.log(val)} // <- this throws an error /> : JSX.Element
|
||||
>ComponentWithUnion : (props: PS | PM) => JSX.Element
|
||||
|
||||
multi={false}
|
||||
>multi : false
|
||||
>false : false
|
||||
|
||||
value={'s'}
|
||||
>value : string
|
||||
>'s' : "s"
|
||||
|
||||
onChange={val => console.log(val)} // <- this throws an error
|
||||
>onChange : (val: string | undefined) => void
|
||||
>val => console.log(val) : (val: string | undefined) => void
|
||||
>val : string | undefined
|
||||
>console.log(val) : void
|
||||
>console.log : (message?: any, ...optionalParams: any[]) => void
|
||||
>console : Console
|
||||
>log : (message?: any, ...optionalParams: any[]) => void
|
||||
>val : string | undefined
|
||||
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// Usage with pure TypeScript
|
||||
ComponentWithUnion({
|
||||
>ComponentWithUnion({ multi: false, value: 's', onChange: val => console.log(val) // <- this works fine}) : JSX.Element
|
||||
>ComponentWithUnion : (props: PS | PM) => JSX.Element
|
||||
>{ multi: false, value: 's', onChange: val => console.log(val) // <- this works fine} : { multi: false; value: string; onChange: (val: string | undefined) => void; }
|
||||
|
||||
multi: false,
|
||||
>multi : false
|
||||
>false : false
|
||||
|
||||
value: 's',
|
||||
>value : string
|
||||
>'s' : "s"
|
||||
|
||||
onChange: val => console.log(val) // <- this works fine
|
||||
>onChange : (val: string | undefined) => void
|
||||
>val => console.log(val) : (val: string | undefined) => void
|
||||
>val : string | undefined
|
||||
>console.log(val) : void
|
||||
>console.log : (message?: any, ...optionalParams: any[]) => void
|
||||
>console : Console
|
||||
>log : (message?: any, ...optionalParams: any[]) => void
|
||||
>val : string | undefined
|
||||
|
||||
});
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
// @jsx: react
|
||||
// @strict: true
|
||||
// @esModuleInterop: true
|
||||
/// <reference path="/.lib/react16.d.ts" />
|
||||
|
||||
import React from 'react';
|
||||
|
||||
interface PS {
|
||||
multi: false
|
||||
value: string | undefined
|
||||
onChange: (selection: string | undefined) => void
|
||||
}
|
||||
|
||||
interface PM {
|
||||
multi: true
|
||||
value: string[]
|
||||
onChange: (selection: string[]) => void
|
||||
}
|
||||
|
||||
export function ComponentWithUnion(props: PM | PS) {
|
||||
return <h1></h1>;
|
||||
}
|
||||
|
||||
// Usage with React tsx
|
||||
export function HereIsTheError() {
|
||||
return (
|
||||
<ComponentWithUnion
|
||||
multi={false}
|
||||
value={'s'}
|
||||
onChange={val => console.log(val)} // <- this throws an error
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// Usage with pure TypeScript
|
||||
ComponentWithUnion({
|
||||
multi: false,
|
||||
value: 's',
|
||||
onChange: val => console.log(val) // <- this works fine
|
||||
});
|
||||
Reference in New Issue
Block a user