Revise discriminateTypeByDiscriminableItems function (#53709)

Co-authored-by: Maria Solano <mariasolano@microsoft.com>
This commit is contained in:
Anders Hejlsberg 2023-04-12 07:25:58 -07:00 committed by GitHub
parent 2db688e36f
commit bec204c844
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 82 additions and 62 deletions

View File

@ -22609,41 +22609,41 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
}
function getBestMatchingType(source: Type, target: UnionOrIntersectionType, isRelatedTo = compareTypesAssignable) {
return findMatchingDiscriminantType(source, target, isRelatedTo, /*skipPartial*/ true) ||
return findMatchingDiscriminantType(source, target, isRelatedTo) ||
findMatchingTypeReferenceOrTypeAliasReference(source, target) ||
findBestTypeForObjectLiteral(source, target) ||
findBestTypeForInvokable(source, target) ||
findMostOverlappyType(source, target);
}
function discriminateTypeByDiscriminableItems(target: UnionType, discriminators: [() => Type, __String][], related: (source: Type, target: Type) => boolean | Ternary, defaultValue?: undefined, skipPartial?: boolean): Type | undefined;
function discriminateTypeByDiscriminableItems(target: UnionType, discriminators: [() => Type, __String][], related: (source: Type, target: Type) => boolean | Ternary, defaultValue: Type, skipPartial?: boolean): Type;
function discriminateTypeByDiscriminableItems(target: UnionType, discriminators: [() => Type, __String][], related: (source: Type, target: Type) => boolean | Ternary, defaultValue?: Type, skipPartial?: boolean) {
// undefined=unknown, true=discriminated, false=not discriminated
// The state of each type progresses from left to right. Discriminated types stop at 'true'.
const discriminable = target.types.map(_ => undefined) as (boolean | undefined)[];
function discriminateTypeByDiscriminableItems(target: UnionType, discriminators: [() => Type, __String][], related: (source: Type, target: Type) => boolean | Ternary) {
const types = target.types;
const include: Ternary[] = types.map(t => t.flags & TypeFlags.Primitive ? Ternary.False : Ternary.True);
for (const [getDiscriminatingType, propertyName] of discriminators) {
const targetProp = getUnionOrIntersectionProperty(target, propertyName);
if (skipPartial && targetProp && getCheckFlags(targetProp) & CheckFlags.ReadPartial) {
continue;
// If the remaining target types include at least one with a matching discriminant, eliminate those that
// have non-matching discriminants. This ensures that we ignore erroneous discriminators and gradually
// refine the target set without eliminating every constituent (which would lead to `never`).
let matched = false;
for (let i = 0; i < types.length; i++) {
if (include[i]) {
const targetType = getTypeOfPropertyOfType(types[i], propertyName);
if (targetType && related(getDiscriminatingType(), targetType)) {
matched = true;
}
else {
include[i] = Ternary.Maybe;
}
}
}
let i = 0;
for (const type of target.types) {
const targetType = getTypeOfPropertyOfType(type, propertyName);
if (targetType && related(getDiscriminatingType(), targetType)) {
discriminable[i] = discriminable[i] === undefined ? true : discriminable[i];
// Turn each Ternary.Maybe into Ternary.False if there was a match. Otherwise, revert to Ternary.True.
for (let i = 0; i < types.length; i++) {
if (include[i] === Ternary.Maybe) {
include[i] = matched ? Ternary.False : Ternary.True;
}
else {
discriminable[i] = false;
}
i++;
}
}
const match = discriminable.indexOf(/*searchElement*/ true);
if (match === -1) {
return defaultValue;
}
return getUnionType(target.types.filter((_, index) => discriminable[index]));
const filtered = contains(include, Ternary.False) ? getUnionType(types.filter((_, i) => include[i])) : target;
return filtered.flags & TypeFlags.Never ? target : filtered;
}
/**
@ -29290,8 +29290,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
s => [() => undefinedType, s.escapedName] as [() => Type, __String]
)
),
isTypeAssignableTo,
contextualType
isTypeAssignableTo
);
}
@ -29307,8 +29306,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
s => [() => undefinedType, s.escapedName] as [() => Type, __String]
)
),
isTypeAssignableTo,
contextualType
isTypeAssignableTo
);
}
@ -48753,7 +48751,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
}
// Keep this up-to-date with the same logic within `getApparentTypeOfContextualType`, since they should behave similarly
function findMatchingDiscriminantType(source: Type, target: Type, isRelatedTo: (source: Type, target: Type) => Ternary, skipPartial?: boolean) {
function findMatchingDiscriminantType(source: Type, target: Type, isRelatedTo: (source: Type, target: Type) => Ternary) {
if (target.flags & TypeFlags.Union && source.flags & (TypeFlags.Intersection | TypeFlags.Object)) {
const match = getMatchingUnionConstituentForType(target as UnionType, source);
if (match) {
@ -48763,7 +48761,10 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
if (sourceProperties) {
const sourcePropertiesFiltered = findDiscriminantProperties(sourceProperties, target);
if (sourcePropertiesFiltered) {
return discriminateTypeByDiscriminableItems(target as UnionType, map(sourcePropertiesFiltered, p => ([() => getTypeOfSymbol(p), p.escapedName] as [() => Type, __String])), isRelatedTo, /*defaultValue*/ undefined, skipPartial);
const discriminated = discriminateTypeByDiscriminableItems(target as UnionType, map(sourcePropertiesFiltered, p => ([() => getTypeOfSymbol(p), p.escapedName] as [() => Type, __String])), isRelatedTo);
if (discriminated !== target) {
return discriminated;
}
}
}
}

View File

@ -1,15 +1,13 @@
tests/cases/conformance/types/typeRelationships/assignmentCompatibility/assignmentCompatWithDiscriminatedUnion.ts(44,5): error TS2322: Type 'S' is not assignable to type 'T'.
Type 'S' is not assignable to type '{ a: 2; b: 3; }'.
Types of property 'a' are incompatible.
Type '0 | 2' is not assignable to type '2'.
Type '0' is not assignable to type '2'.
Type 'S' is not assignable to type '{ a: 0; b: 4 | 1; } | { a: 1; b: 2 | 4; }'.
Type 'S' is not assignable to type '{ a: 1; b: 2 | 4; }'.
Types of property 'a' are incompatible.
Type '0 | 2' is not assignable to type '1'.
Type '0' is not assignable to type '1'.
tests/cases/conformance/types/typeRelationships/assignmentCompatibility/assignmentCompatWithDiscriminatedUnion.ts(58,5): error TS2322: Type 'S' is not assignable to type 'T'.
Property 'c' is missing in type 'S' but required in type '{ a: 2; b: 4 | 3; c: string; }'.
Type 'S' is not assignable to type '{ a: 0; b: 4 | 1; } | { a: 2; b: 4 | 3; c: string; }'.
Property 'c' is missing in type 'S' but required in type '{ a: 2; b: 4 | 3; c: string; }'.
tests/cases/conformance/types/typeRelationships/assignmentCompatibility/assignmentCompatWithDiscriminatedUnion.ts(82,5): error TS2322: Type 'S' is not assignable to type 'T'.
Type 'S' is not assignable to type '{ a: N; b: N; c: 2; }'.
Types of property 'c' are incompatible.
Type 'N' is not assignable to type '2'.
Type '0' is not assignable to type '2'.
==== tests/cases/conformance/types/typeRelationships/assignmentCompatibility/assignmentCompatWithDiscriminatedUnion.ts (3 errors) ====
@ -59,10 +57,11 @@ tests/cases/conformance/types/typeRelationships/assignmentCompatibility/assignme
t = s;
~
!!! error TS2322: Type 'S' is not assignable to type 'T'.
!!! error TS2322: Type 'S' is not assignable to type '{ a: 2; b: 3; }'.
!!! error TS2322: Types of property 'a' are incompatible.
!!! error TS2322: Type '0 | 2' is not assignable to type '2'.
!!! error TS2322: Type '0' is not assignable to type '2'.
!!! error TS2322: Type 'S' is not assignable to type '{ a: 0; b: 4 | 1; } | { a: 1; b: 2 | 4; }'.
!!! error TS2322: Type 'S' is not assignable to type '{ a: 1; b: 2 | 4; }'.
!!! error TS2322: Types of property 'a' are incompatible.
!!! error TS2322: Type '0 | 2' is not assignable to type '1'.
!!! error TS2322: Type '0' is not assignable to type '1'.
}
// Unmatched non-discriminants
@ -79,7 +78,8 @@ tests/cases/conformance/types/typeRelationships/assignmentCompatibility/assignme
t = s;
~
!!! error TS2322: Type 'S' is not assignable to type 'T'.
!!! error TS2322: Property 'c' is missing in type 'S' but required in type '{ a: 2; b: 4 | 3; c: string; }'.
!!! error TS2322: Type 'S' is not assignable to type '{ a: 0; b: 4 | 1; } | { a: 2; b: 4 | 3; c: string; }'.
!!! error TS2322: Property 'c' is missing in type 'S' but required in type '{ a: 2; b: 4 | 3; c: string; }'.
!!! related TS2728 tests/cases/conformance/types/typeRelationships/assignmentCompatibility/assignmentCompatWithDiscriminatedUnion.ts:52:36: 'c' is declared here.
}
@ -107,10 +107,6 @@ tests/cases/conformance/types/typeRelationships/assignmentCompatibility/assignme
t = s;
~
!!! error TS2322: Type 'S' is not assignable to type 'T'.
!!! error TS2322: Type 'S' is not assignable to type '{ a: N; b: N; c: 2; }'.
!!! error TS2322: Types of property 'c' are incompatible.
!!! error TS2322: Type 'N' is not assignable to type '2'.
!!! error TS2322: Type '0' is not assignable to type '2'.
}
// https://github.com/Microsoft/TypeScript/issues/14865

View File

@ -10,8 +10,10 @@ tests/cases/compiler/excessPropertyCheckWithMultipleDiscriminants.ts(83,5): erro
Object literal may only specify known properties, and 'b' does not exist in type 'Common | (Common & A)'.
tests/cases/compiler/excessPropertyCheckWithMultipleDiscriminants.ts(93,5): error TS2322: Type '{ type: "A"; n: number; a: number; b: number; }' is not assignable to type 'CommonWithDisjointOverlappingOptionals'.
Object literal may only specify known properties, and 'b' does not exist in type 'Common | A'.
tests/cases/compiler/excessPropertyCheckWithMultipleDiscriminants.ts(130,5): error TS2322: Type '"string"' is not assignable to type '"number"'.
tests/cases/compiler/excessPropertyCheckWithMultipleDiscriminants.ts(136,5): error TS2322: Type '"string"' is not assignable to type '"number"'.
tests/cases/compiler/excessPropertyCheckWithMultipleDiscriminants.ts(131,5): error TS2322: Type '{ type: "string"; autoIncrement: boolean; required: true; }' is not assignable to type 'Attribute'.
Object literal may only specify known properties, and 'autoIncrement' does not exist in type 'StringAttribute | OneToOneAttribute'.
tests/cases/compiler/excessPropertyCheckWithMultipleDiscriminants.ts(137,5): error TS2322: Type '{ type: "string"; autoIncrement: boolean; required: true; }' is not assignable to type 'Attribute2'.
Object literal may only specify known properties, and 'autoIncrement' does not exist in type 'StringAttribute'.
==== tests/cases/compiler/excessPropertyCheckWithMultipleDiscriminants.ts (8 errors) ====
@ -163,17 +165,19 @@ tests/cases/compiler/excessPropertyCheckWithMultipleDiscriminants.ts(136,5): err
// both should error due to excess properties
const attributes: Attribute = {
type: 'string',
~~~~
!!! error TS2322: Type '"string"' is not assignable to type '"number"'.
autoIncrement: true,
~~~~~~~~~~~~~
!!! error TS2322: Type '{ type: "string"; autoIncrement: boolean; required: true; }' is not assignable to type 'Attribute'.
!!! error TS2322: Object literal may only specify known properties, and 'autoIncrement' does not exist in type 'StringAttribute | OneToOneAttribute'.
required: true,
};
const attributes2: Attribute2 = {
type: 'string',
~~~~
!!! error TS2322: Type '"string"' is not assignable to type '"number"'.
autoIncrement: true,
~~~~~~~~~~~~~
!!! error TS2322: Type '{ type: "string"; autoIncrement: boolean; required: true; }' is not assignable to type 'Attribute2'.
!!! error TS2322: Object literal may only specify known properties, and 'autoIncrement' does not exist in type 'StringAttribute'.
required: true,
};

View File

@ -17,7 +17,8 @@ tests/cases/compiler/jsxComponentTypeErrors.tsx(27,16): error TS2786: 'ClassComp
tests/cases/compiler/jsxComponentTypeErrors.tsx(28,16): error TS2786: 'MixedComponent' cannot be used as a JSX component.
Its element type 'ClassComponent | { type: string | undefined; }' is not a valid JSX element.
Type 'ClassComponent' is not assignable to type 'Element | ElementClass | null'.
Type 'ClassComponent' is not assignable to type 'ElementClass'.
Type 'ClassComponent' is not assignable to type 'Element | ElementClass'.
Type 'ClassComponent' is not assignable to type 'ElementClass'.
tests/cases/compiler/jsxComponentTypeErrors.tsx(37,16): error TS2786: 'obj.MemberFunctionComponent' cannot be used as a JSX component.
Its return type '{}' is not a valid JSX element.
Property 'type' is missing in type '{}' but required in type 'Element'.
@ -79,7 +80,8 @@ tests/cases/compiler/jsxComponentTypeErrors.tsx(38,16): error TS2786: 'obj. Memb
!!! error TS2786: 'MixedComponent' cannot be used as a JSX component.
!!! error TS2786: Its element type 'ClassComponent | { type: string | undefined; }' is not a valid JSX element.
!!! error TS2786: Type 'ClassComponent' is not assignable to type 'Element | ElementClass | null'.
!!! error TS2786: Type 'ClassComponent' is not assignable to type 'ElementClass'.
!!! error TS2786: Type 'ClassComponent' is not assignable to type 'Element | ElementClass'.
!!! error TS2786: Type 'ClassComponent' is not assignable to type 'ElementClass'.
const obj = {
MemberFunctionComponent() {

View File

@ -5,9 +5,8 @@ tests/cases/conformance/expressions/objectLiterals/objectLiteralNormalization.ts
tests/cases/conformance/expressions/objectLiterals/objectLiteralNormalization.ts(9,1): error TS2322: Type '{ c: true; }' is not assignable to type '{ a: number; b?: undefined; c?: undefined; } | { a: number; b: string; c?: undefined; } | { a: number; b: string; c: boolean; }'.
Type '{ c: true; }' is missing the following properties from type '{ a: number; b: string; c: boolean; }': a, b
tests/cases/conformance/expressions/objectLiterals/objectLiteralNormalization.ts(17,1): error TS2322: Type '{ a: string; b: number; }' is not assignable to type '{ a: number; b: number; } | { a: string; b?: undefined; } | { a?: undefined; b?: undefined; }'.
Type '{ a: string; b: number; }' is not assignable to type '{ a?: undefined; b?: undefined; }'.
Types of property 'a' are incompatible.
Type 'string' is not assignable to type 'undefined'.
Types of property 'b' are incompatible.
Type 'number' is not assignable to type 'undefined'.
tests/cases/conformance/expressions/objectLiterals/objectLiteralNormalization.ts(18,1): error TS2322: Type '{ a: number; }' is not assignable to type '{ a: number; b: number; } | { a: string; b?: undefined; } | { a?: undefined; b?: undefined; }'.
Property 'b' is missing in type '{ a: number; }' but required in type '{ a: number; b: number; }'.
tests/cases/conformance/expressions/objectLiterals/objectLiteralNormalization.ts(48,20): error TS2322: Type '2' is not assignable to type '1'.
@ -44,9 +43,8 @@ tests/cases/conformance/expressions/objectLiterals/objectLiteralNormalization.ts
a2 = { a: "def", b: 20 }; // Error
~~
!!! error TS2322: Type '{ a: string; b: number; }' is not assignable to type '{ a: number; b: number; } | { a: string; b?: undefined; } | { a?: undefined; b?: undefined; }'.
!!! error TS2322: Type '{ a: string; b: number; }' is not assignable to type '{ a?: undefined; b?: undefined; }'.
!!! error TS2322: Types of property 'a' are incompatible.
!!! error TS2322: Type 'string' is not assignable to type 'undefined'.
!!! error TS2322: Types of property 'b' are incompatible.
!!! error TS2322: Type 'number' is not assignable to type 'undefined'.
a2 = { a: 1 }; // Error
~~
!!! error TS2322: Type '{ a: number; }' is not assignable to type '{ a: number; b: number; } | { a: string; b?: undefined; } | { a?: undefined; b?: undefined; }'.

View File

@ -0,0 +1,19 @@
/// <reference path="fourslash.ts" />
//// type Foo = { a: 0, b: 'x' } | { a: 0, b: 'y' } | { a: 1, b: 'z' };
//// const foo: Foo = { a: 0, b: '/*1*/' }
////
//// type Bar = { a: 0, b: 'fx' } | { a: 0, b: 'fy' } | { a: 1, b: 'fz' };
//// const bar: Bar = { a: 0, b: 'f/*2*/' }
////
//// type Baz = { x: 0, y: 0, z: 'a' } | { x: 0, y: 1, z: 'b' } | { x: 1, y: 0, z: 'c' } | { x: 1, y: 1, z: 'd' };
//// const baz1: Baz = { z: '/*3*/' };
//// const baz2: Baz = { x: 0, z: '/*4*/' };
//// const baz3: Baz = { x: 0, y: 1, z: '/*5*/' };
//// const baz4: Baz = { x: 2, y: 1, z: '/*6*/' };
verify.completions({ marker: "1", triggerCharacter: "'", includes: [ "x", "y" ], excludes: [ "z" ] });
verify.completions({ marker: "2", triggerCharacter: "'", includes: [ "fx", "fy" ], excludes: [ "fz" ] });
verify.completions({ marker: "3", triggerCharacter: "'", includes: [ "a", "b", "c", "d" ] });
verify.completions({ marker: "4", triggerCharacter: "'", includes: [ "a", "b" ], excludes: [ "c", "d" ] });
verify.completions({ marker: "5", triggerCharacter: "'", includes: [ "b" ], excludes: [ "a", "c", "d" ] });
verify.completions({ marker: "6", triggerCharacter: "'", includes: [ "b", "d" ], excludes: [ "a", "c" ] });