Skip to content

Commit aa9eed6

Browse files
weswighamOrta
andcommitted
Merge identical object types when discriminating contextual types
Co-authored-by: Orta <[email protected]>
1 parent 23cb2d8 commit aa9eed6

7 files changed

+186
-10
lines changed

src/compiler/checker.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12972,6 +12972,26 @@ namespace ts {
1297212972
return true;
1297312973
}
1297412974

12975+
function removeStructurallyIdenticalTypes(types: Type[]): void {
12976+
const len = types.length;
12977+
if (len === 0 || isSetOfLiteralsFromSameEnum(types)) {
12978+
return;
12979+
}
12980+
let i = len;
12981+
while (i > 0) {
12982+
i--;
12983+
const source = types[i];
12984+
for (const target of types) {
12985+
if (source !== target) {
12986+
if (isTypeIdenticalTo(source, target)) {
12987+
orderedRemoveItemAt(types, i);
12988+
break;
12989+
}
12990+
}
12991+
}
12992+
}
12993+
}
12994+
1297512995
function removeRedundantLiteralTypes(types: Type[], includes: TypeFlags) {
1297612996
let i = types.length;
1297712997
while (i > 0) {
@@ -13006,7 +13026,7 @@ namespace ts {
1300613026
const typeSet: Type[] = [];
1300713027
const includes = addTypesToUnion(typeSet, 0, types);
1300813028
if (unionReduction !== UnionReduction.None) {
13009-
if (includes & TypeFlags.AnyOrUnknown) {
13029+
if (includes & TypeFlags.AnyOrUnknown && unionReduction !== UnionReduction.Exact) {
1301013030
return includes & TypeFlags.Any ? includes & TypeFlags.IncludesWildcard ? wildcardType : anyType : unknownType;
1301113031
}
1301213032
switch (unionReduction) {
@@ -13020,6 +13040,9 @@ namespace ts {
1302013040
return errorType;
1302113041
}
1302213042
break;
13043+
case UnionReduction.Exact:
13044+
removeStructurallyIdenticalTypes(typeSet);
13045+
break;
1302313046
}
1302413047
if (typeSet.length === 0) {
1302513048
return includes & TypeFlags.Null ? includes & TypeFlags.IncludesNonWideningType ? nullType : nullWideningType :
@@ -18243,6 +18266,11 @@ namespace ts {
1824318266
function discriminateTypeByDiscriminableItems(target: UnionType, discriminators: [() => Type, __String][], related: (source: Type, target: Type) => boolean | Ternary, defaultValue?: undefined, skipPartial?: boolean): Type | undefined;
1824418267
function discriminateTypeByDiscriminableItems(target: UnionType, discriminators: [() => Type, __String][], related: (source: Type, target: Type) => boolean | Ternary, defaultValue: Type, skipPartial?: boolean): Type;
1824518268
function discriminateTypeByDiscriminableItems(target: UnionType, discriminators: [() => Type, __String][], related: (source: Type, target: Type) => boolean | Ternary, defaultValue?: Type, skipPartial?: boolean) {
18269+
const reduced = getUnionType(target.types, UnionReduction.Exact, target.aliasSymbol, target.aliasTypeArguments);
18270+
if (!(reduced.flags & TypeFlags.Union)) {
18271+
return reduced;
18272+
}
18273+
target = reduced as UnionType;
1824618274
// undefined=unknown, true=discriminated, false=not discriminated
1824718275
// The state of each type progresses from left to right. Discriminated types stop at 'true'.
1824818276
const discriminable = target.types.map(_ => undefined) as (boolean | undefined)[];

src/compiler/types.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4184,7 +4184,8 @@ namespace ts {
41844184
export const enum UnionReduction {
41854185
None = 0,
41864186
Literal,
4187-
Subtype
4187+
Subtype,
4188+
Exact,
41884189
}
41894190

41904191
/* @internal */

tests/baselines/reference/contextualTypeWithUnionTypeObjectLiteral.errors.txt

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,10 @@ tests/cases/conformance/types/union/contextualTypeWithUnionTypeObjectLiteral.ts(
2323
Types of property 'prop' are incompatible.
2424
Type 'string | number' is not assignable to type 'number'.
2525
Type 'string' is not assignable to type 'number'.
26-
tests/cases/conformance/types/union/contextualTypeWithUnionTypeObjectLiteral.ts(58,5): error TS2322: Type '(a: string, b: number) => string | number' is not assignable to type '((a: string, b: number) => string) | ((a: string, b: number) => number)'.
27-
Type '(a: string, b: number) => string | number' is not assignable to type '(a: string, b: number) => string'.
28-
Type 'string | number' is not assignable to type 'string'.
29-
Type 'number' is not assignable to type 'string'.
26+
tests/cases/conformance/types/union/contextualTypeWithUnionTypeObjectLiteral.ts(58,5): error TS2322: Type '(a: string, b: number) => string | number' is not assignable to type '((a: string, b: number) => number) | ((a: string, b: number) => string)'.
27+
Type '(a: string, b: number) => string | number' is not assignable to type '(a: string, b: number) => number'.
28+
Type 'string | number' is not assignable to type 'number'.
29+
Type 'string' is not assignable to type 'number'.
3030

3131

3232
==== tests/cases/conformance/types/union/contextualTypeWithUnionTypeObjectLiteral.ts (6 errors) ====
@@ -119,9 +119,9 @@ tests/cases/conformance/types/union/contextualTypeWithUnionTypeObjectLiteral.ts(
119119
var i11Ori21: I11 | I21 = { // Like i1 and i2 both
120120
commonMethodDifferentReturnType: (a, b) => strOrNumber,
121121
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
122-
!!! error TS2322: Type '(a: string, b: number) => string | number' is not assignable to type '((a: string, b: number) => string) | ((a: string, b: number) => number)'.
123-
!!! error TS2322: Type '(a: string, b: number) => string | number' is not assignable to type '(a: string, b: number) => string'.
124-
!!! error TS2322: Type 'string | number' is not assignable to type 'string'.
125-
!!! error TS2322: Type 'number' is not assignable to type 'string'.
122+
!!! error TS2322: Type '(a: string, b: number) => string | number' is not assignable to type '((a: string, b: number) => number) | ((a: string, b: number) => string)'.
123+
!!! error TS2322: Type '(a: string, b: number) => string | number' is not assignable to type '(a: string, b: number) => number'.
124+
!!! error TS2322: Type 'string | number' is not assignable to type 'number'.
125+
!!! error TS2322: Type 'string' is not assignable to type 'number'.
126126
!!! related TS6500 tests/cases/conformance/types/union/contextualTypeWithUnionTypeObjectLiteral.ts:35:5: The expected type comes from property 'commonMethodDifferentReturnType' which is declared here on type 'I11 | I21'
127127
};
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
//// [inferenceUnionOfObjectsMappedContextualType.ts]
2+
type Entity = {
3+
someDate: Date | null;
4+
} & ({ id: string; } | { id: number; })
5+
6+
type RowRendererMeta<TInput extends {}> = {
7+
[key in keyof TInput]: { key: key; caption: string; formatter?: (value: TInput[key]) => string; };
8+
}
9+
10+
type RowRenderer<TInput extends {}> = RowRendererMeta<TInput>[keyof RowRendererMeta<TInput>];
11+
12+
const test: RowRenderer<Entity> = {
13+
key: 'someDate',
14+
caption: 'My Date',
15+
formatter: (value) => value ? value.toString() : '-' // value: any
16+
}
17+
18+
19+
//// [inferenceUnionOfObjectsMappedContextualType.js]
20+
"use strict";
21+
var test = {
22+
key: 'someDate',
23+
caption: 'My Date',
24+
formatter: function (value) { return value ? value.toString() : '-'; } // value: any
25+
};
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
=== tests/cases/compiler/inferenceUnionOfObjectsMappedContextualType.ts ===
2+
type Entity = {
3+
>Entity : Symbol(Entity, Decl(inferenceUnionOfObjectsMappedContextualType.ts, 0, 0))
4+
5+
someDate: Date | null;
6+
>someDate : Symbol(someDate, Decl(inferenceUnionOfObjectsMappedContextualType.ts, 0, 15))
7+
>Date : Symbol(Date, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --), Decl(lib.scripthost.d.ts, --, --))
8+
9+
} & ({ id: string; } | { id: number; })
10+
>id : Symbol(id, Decl(inferenceUnionOfObjectsMappedContextualType.ts, 2, 6))
11+
>id : Symbol(id, Decl(inferenceUnionOfObjectsMappedContextualType.ts, 2, 24))
12+
13+
type RowRendererMeta<TInput extends {}> = {
14+
>RowRendererMeta : Symbol(RowRendererMeta, Decl(inferenceUnionOfObjectsMappedContextualType.ts, 2, 39))
15+
>TInput : Symbol(TInput, Decl(inferenceUnionOfObjectsMappedContextualType.ts, 4, 21))
16+
17+
[key in keyof TInput]: { key: key; caption: string; formatter?: (value: TInput[key]) => string; };
18+
>key : Symbol(key, Decl(inferenceUnionOfObjectsMappedContextualType.ts, 5, 5))
19+
>TInput : Symbol(TInput, Decl(inferenceUnionOfObjectsMappedContextualType.ts, 4, 21))
20+
>key : Symbol(key, Decl(inferenceUnionOfObjectsMappedContextualType.ts, 5, 28))
21+
>key : Symbol(key, Decl(inferenceUnionOfObjectsMappedContextualType.ts, 5, 5))
22+
>caption : Symbol(caption, Decl(inferenceUnionOfObjectsMappedContextualType.ts, 5, 38))
23+
>formatter : Symbol(formatter, Decl(inferenceUnionOfObjectsMappedContextualType.ts, 5, 55))
24+
>value : Symbol(value, Decl(inferenceUnionOfObjectsMappedContextualType.ts, 5, 69))
25+
>TInput : Symbol(TInput, Decl(inferenceUnionOfObjectsMappedContextualType.ts, 4, 21))
26+
>key : Symbol(key, Decl(inferenceUnionOfObjectsMappedContextualType.ts, 5, 5))
27+
}
28+
29+
type RowRenderer<TInput extends {}> = RowRendererMeta<TInput>[keyof RowRendererMeta<TInput>];
30+
>RowRenderer : Symbol(RowRenderer, Decl(inferenceUnionOfObjectsMappedContextualType.ts, 6, 1))
31+
>TInput : Symbol(TInput, Decl(inferenceUnionOfObjectsMappedContextualType.ts, 8, 17))
32+
>RowRendererMeta : Symbol(RowRendererMeta, Decl(inferenceUnionOfObjectsMappedContextualType.ts, 2, 39))
33+
>TInput : Symbol(TInput, Decl(inferenceUnionOfObjectsMappedContextualType.ts, 8, 17))
34+
>RowRendererMeta : Symbol(RowRendererMeta, Decl(inferenceUnionOfObjectsMappedContextualType.ts, 2, 39))
35+
>TInput : Symbol(TInput, Decl(inferenceUnionOfObjectsMappedContextualType.ts, 8, 17))
36+
37+
const test: RowRenderer<Entity> = {
38+
>test : Symbol(test, Decl(inferenceUnionOfObjectsMappedContextualType.ts, 10, 5))
39+
>RowRenderer : Symbol(RowRenderer, Decl(inferenceUnionOfObjectsMappedContextualType.ts, 6, 1))
40+
>Entity : Symbol(Entity, Decl(inferenceUnionOfObjectsMappedContextualType.ts, 0, 0))
41+
42+
key: 'someDate',
43+
>key : Symbol(key, Decl(inferenceUnionOfObjectsMappedContextualType.ts, 10, 35))
44+
45+
caption: 'My Date',
46+
>caption : Symbol(caption, Decl(inferenceUnionOfObjectsMappedContextualType.ts, 11, 20))
47+
48+
formatter: (value) => value ? value.toString() : '-' // value: any
49+
>formatter : Symbol(formatter, Decl(inferenceUnionOfObjectsMappedContextualType.ts, 12, 23))
50+
>value : Symbol(value, Decl(inferenceUnionOfObjectsMappedContextualType.ts, 13, 16))
51+
>value : Symbol(value, Decl(inferenceUnionOfObjectsMappedContextualType.ts, 13, 16))
52+
>value.toString : Symbol(Date.toString, Decl(lib.es5.d.ts, --, --))
53+
>value : Symbol(value, Decl(inferenceUnionOfObjectsMappedContextualType.ts, 13, 16))
54+
>toString : Symbol(Date.toString, Decl(lib.es5.d.ts, --, --))
55+
}
56+
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
=== tests/cases/compiler/inferenceUnionOfObjectsMappedContextualType.ts ===
2+
type Entity = {
3+
>Entity : Entity
4+
5+
someDate: Date | null;
6+
>someDate : Date | null
7+
>null : null
8+
9+
} & ({ id: string; } | { id: number; })
10+
>id : string
11+
>id : number
12+
13+
type RowRendererMeta<TInput extends {}> = {
14+
>RowRendererMeta : RowRendererMeta<TInput>
15+
16+
[key in keyof TInput]: { key: key; caption: string; formatter?: (value: TInput[key]) => string; };
17+
>key : key
18+
>caption : string
19+
>formatter : ((value: TInput[key]) => string) | undefined
20+
>value : TInput[key]
21+
}
22+
23+
type RowRenderer<TInput extends {}> = RowRendererMeta<TInput>[keyof RowRendererMeta<TInput>];
24+
>RowRenderer : RowRenderer<TInput>
25+
26+
const test: RowRenderer<Entity> = {
27+
>test : RowRenderer<Entity>
28+
>{ key: 'someDate', caption: 'My Date', formatter: (value) => value ? value.toString() : '-' // value: any} : { key: "someDate"; caption: string; formatter: (value: Date | null) => string; }
29+
30+
key: 'someDate',
31+
>key : "someDate"
32+
>'someDate' : "someDate"
33+
34+
caption: 'My Date',
35+
>caption : string
36+
>'My Date' : "My Date"
37+
38+
formatter: (value) => value ? value.toString() : '-' // value: any
39+
>formatter : (value: Date | null) => string
40+
>(value) => value ? value.toString() : '-' : (value: Date | null) => string
41+
>value : Date | null
42+
>value ? value.toString() : '-' : string
43+
>value : Date | null
44+
>value.toString() : string
45+
>value.toString : () => string
46+
>value : Date
47+
>toString : () => string
48+
>'-' : "-"
49+
}
50+
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// @strict: true
2+
type Entity = {
3+
someDate: Date | null;
4+
} & ({ id: string; } | { id: number; })
5+
6+
type RowRendererMeta<TInput extends {}> = {
7+
[key in keyof TInput]: { key: key; caption: string; formatter?: (value: TInput[key]) => string; };
8+
}
9+
10+
type RowRenderer<TInput extends {}> = RowRendererMeta<TInput>[keyof RowRendererMeta<TInput>];
11+
12+
const test: RowRenderer<Entity> = {
13+
key: 'someDate',
14+
caption: 'My Date',
15+
formatter: (value) => value ? value.toString() : '-' // value: any
16+
}

0 commit comments

Comments
 (0)