Skip to content

Commit 180b94d

Browse files
committed
Improve error messages for discriminated union incompatible members
Fixes #62737 When assigning objects with wider discriminator unions to discriminated union types, TypeScript now identifies the actual incompatible types from the source union, rather than showing a misleading error about a compatible type. Before: Type '"foo"' is not assignable to type '"bar"' After: The type(s) '"baz"' are not present in the discriminator 'discriminator' of the target. This change: - Adds detection for discriminated union assignment errors - Reports which specific types from the source are missing in the target - Includes the discriminator property name in the error message - Adds comprehensive test cases
1 parent 4ef2378 commit 180b94d

12 files changed

Lines changed: 633 additions & 0 deletions

src/compiler/checker.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23096,6 +23096,41 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
2309623096
}
2309723097
}
2309823098
if (reportErrors) {
23099+
// Check if we're dealing with a discriminated union case where we can provide better error messages
23100+
if (target.flags & TypeFlags.Union && source.flags & TypeFlags.Object) {
23101+
const discriminantProps = findDiscriminantProperties(getPropertiesOfType(source), target);
23102+
if (discriminantProps && discriminantProps.length > 0) {
23103+
// For each discriminant property, check if the source property type contains values
23104+
// not present in any constituent of the target union
23105+
for (const prop of discriminantProps) {
23106+
const sourcePropType = getTypeOfSymbol(prop);
23107+
if (sourcePropType.flags & TypeFlags.Union) {
23108+
// Get all discriminant values from the target union for this property
23109+
const targetDiscriminantTypes: Type[] = [];
23110+
for (const constituent of (target as UnionType).types) {
23111+
const constituentPropType = getTypeOfPropertyOfType(constituent, prop.escapedName);
23112+
if (constituentPropType) {
23113+
forEachType(constituentPropType, t => { targetDiscriminantTypes.push(t); });
23114+
}
23115+
}
23116+
const targetDiscriminantUnion = getUnionType(targetDiscriminantTypes);
23117+
// Find source types that are not assignable to the target discriminant union
23118+
const incompatibleTypes: Type[] = [];
23119+
for (const sourceType of (sourcePropType as UnionType).types) {
23120+
if (!isTypeAssignableTo(sourceType, targetDiscriminantUnion)) {
23121+
incompatibleTypes.push(sourceType);
23122+
}
23123+
}
23124+
if (incompatibleTypes.length > 0) {
23125+
const incompatibleTypesStr = incompatibleTypes.map(t => typeToString(t)).join(" | ");
23126+
const propName = symbolToString(prop);
23127+
reportError(Diagnostics.Type_0_is_not_assignable_to_type_1_The_type_s_2_are_not_present_in_the_discriminator_3_of_the_target, typeToString(source), typeToString(target), incompatibleTypesStr, propName);
23128+
return Ternary.False;
23129+
}
23130+
}
23131+
}
23132+
}
23133+
}
2309923134
// Elaborate only if we can find a best matching type in the target union
2310023135
const bestMatchingType = getBestMatchingType(source, target, isRelatedTo);
2310123136
if (bestMatchingType) {

src/compiler/diagnosticMessages.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4023,6 +4023,14 @@
40234023
"category": "Error",
40244024
"code": 2882
40254025
},
4026+
"Type '{0}' is not assignable to type '{1}'. The types of '{2}' are incompatible between these types.": {
4027+
"category": "Error",
4028+
"code": 2883
4029+
},
4030+
"Type '{0}' is not assignable to type '{1}'. The type(s) '{2}' are not present in the discriminator '{3}' of the target.": {
4031+
"category": "Error",
4032+
"code": 2884
4033+
},
40264034

40274035
"Import declaration '{0}' is using private name '{1}'.": {
40284036
"category": "Error",
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
discriminatedUnionIncompatibleMemberErrorMessage.ts(12,7): error TS2322: Type '{ discriminator: UnionType; }' is not assignable to type 'Discriminated'.
2+
Type '{ discriminator: UnionType; }' is not assignable to type 'Discriminated'. The type(s) '"baz"' are not present in the discriminator 'discriminator' of the target.
3+
discriminatedUnionIncompatibleMemberErrorMessage.ts(17,7): error TS2322: Type '{ discriminator: WiderUnion; }' is not assignable to type 'Discriminated'.
4+
Type '{ discriminator: WiderUnion; }' is not assignable to type 'Discriminated'. The type(s) '"baz" | "qux"' are not present in the discriminator 'discriminator' of the target.
5+
discriminatedUnionIncompatibleMemberErrorMessage.ts(26,7): error TS2322: Type '{ kind: ShapeKind; }' is not assignable to type 'Shape'.
6+
Type '{ kind: ShapeKind; }' is not assignable to type 'Shape'. The type(s) '"triangle"' are not present in the discriminator 'kind' of the target.
7+
discriminatedUnionIncompatibleMemberErrorMessage.ts(33,7): error TS2322: Type '{ discriminator: UnionType; }' is not assignable to type 'Discriminated2'.
8+
Type '{ discriminator: UnionType; }' is not assignable to type 'Discriminated2'. The type(s) '"baz"' are not present in the discriminator 'discriminator' of the target.
9+
10+
11+
==== discriminatedUnionIncompatibleMemberErrorMessage.ts (4 errors) ====
12+
// Test case for issue #62737
13+
// Improve error messages when assigning objects with wider discriminator unions to discriminated union types
14+
15+
// Basic case - single incompatible type
16+
type Discriminated =
17+
| { discriminator: "foo" }
18+
| { discriminator: "bar" };
19+
20+
type UnionType = "foo" | "bar" | "baz";
21+
22+
const obj = { discriminator: "foo" as UnionType };
23+
const err: Discriminated = obj; // Error: "baz" is not present in discriminator
24+
~~~
25+
!!! error TS2322: Type '{ discriminator: UnionType; }' is not assignable to type 'Discriminated'.
26+
!!! error TS2322: Type '{ discriminator: UnionType; }' is not assignable to type 'Discriminated'. The type(s) '"baz"' are not present in the discriminator 'discriminator' of the target.
27+
28+
// Multiple incompatible types
29+
type WiderUnion = "foo" | "bar" | "baz" | "qux";
30+
const obj2 = { discriminator: "foo" as WiderUnion };
31+
const err2: Discriminated = obj2; // Error: "baz" | "qux" are not present in discriminator
32+
~~~~
33+
!!! error TS2322: Type '{ discriminator: WiderUnion; }' is not assignable to type 'Discriminated'.
34+
!!! error TS2322: Type '{ discriminator: WiderUnion; }' is not assignable to type 'Discriminated'. The type(s) '"baz" | "qux"' are not present in the discriminator 'discriminator' of the target.
35+
36+
// Different discriminator property name
37+
type Shape =
38+
| { kind: "circle"; radius: number }
39+
| { kind: "square"; side: number };
40+
41+
type ShapeKind = "circle" | "square" | "triangle";
42+
const shape = { kind: "circle" as ShapeKind };
43+
const err3: Shape = shape; // Error: "triangle" is not present in discriminator "kind"
44+
~~~~
45+
!!! error TS2322: Type '{ kind: ShapeKind; }' is not assignable to type 'Shape'.
46+
!!! error TS2322: Type '{ kind: ShapeKind; }' is not assignable to type 'Shape'. The type(s) '"triangle"' are not present in the discriminator 'kind' of the target.
47+
48+
// From issue #62603
49+
type Discriminated2 =
50+
| { discriminator: "foo" }
51+
| { discriminator: "bar" };
52+
53+
const unexpectedError: Discriminated2 = { discriminator: "foo" } as { discriminator: UnionType };
54+
~~~~~~~~~~~~~~~
55+
!!! error TS2322: Type '{ discriminator: UnionType; }' is not assignable to type 'Discriminated2'.
56+
!!! error TS2322: Type '{ discriminator: UnionType; }' is not assignable to type 'Discriminated2'. The type(s) '"baz"' are not present in the discriminator 'discriminator' of the target.
57+
58+
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
//// [tests/cases/compiler/discriminatedUnionIncompatibleMemberErrorMessage.ts] ////
2+
3+
//// [discriminatedUnionIncompatibleMemberErrorMessage.ts]
4+
// Test case for issue #62737
5+
// Improve error messages when assigning objects with wider discriminator unions to discriminated union types
6+
7+
// Basic case - single incompatible type
8+
type Discriminated =
9+
| { discriminator: "foo" }
10+
| { discriminator: "bar" };
11+
12+
type UnionType = "foo" | "bar" | "baz";
13+
14+
const obj = { discriminator: "foo" as UnionType };
15+
const err: Discriminated = obj; // Error: "baz" is not present in discriminator
16+
17+
// Multiple incompatible types
18+
type WiderUnion = "foo" | "bar" | "baz" | "qux";
19+
const obj2 = { discriminator: "foo" as WiderUnion };
20+
const err2: Discriminated = obj2; // Error: "baz" | "qux" are not present in discriminator
21+
22+
// Different discriminator property name
23+
type Shape =
24+
| { kind: "circle"; radius: number }
25+
| { kind: "square"; side: number };
26+
27+
type ShapeKind = "circle" | "square" | "triangle";
28+
const shape = { kind: "circle" as ShapeKind };
29+
const err3: Shape = shape; // Error: "triangle" is not present in discriminator "kind"
30+
31+
// From issue #62603
32+
type Discriminated2 =
33+
| { discriminator: "foo" }
34+
| { discriminator: "bar" };
35+
36+
const unexpectedError: Discriminated2 = { discriminator: "foo" } as { discriminator: UnionType };
37+
38+
39+
40+
//// [discriminatedUnionIncompatibleMemberErrorMessage.js]
41+
"use strict";
42+
// Test case for issue #62737
43+
// Improve error messages when assigning objects with wider discriminator unions to discriminated union types
44+
var obj = { discriminator: "foo" };
45+
var err = obj; // Error: "baz" is not present in discriminator
46+
var obj2 = { discriminator: "foo" };
47+
var err2 = obj2; // Error: "baz" | "qux" are not present in discriminator
48+
var shape = { kind: "circle" };
49+
var err3 = shape; // Error: "triangle" is not present in discriminator "kind"
50+
var unexpectedError = { discriminator: "foo" };
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
//// [tests/cases/compiler/discriminatedUnionIncompatibleMemberErrorMessage.ts] ////
2+
3+
=== discriminatedUnionIncompatibleMemberErrorMessage.ts ===
4+
// Test case for issue #62737
5+
// Improve error messages when assigning objects with wider discriminator unions to discriminated union types
6+
7+
// Basic case - single incompatible type
8+
type Discriminated =
9+
>Discriminated : Symbol(Discriminated, Decl(discriminatedUnionIncompatibleMemberErrorMessage.ts, 0, 0))
10+
11+
| { discriminator: "foo" }
12+
>discriminator : Symbol(discriminator, Decl(discriminatedUnionIncompatibleMemberErrorMessage.ts, 5, 5))
13+
14+
| { discriminator: "bar" };
15+
>discriminator : Symbol(discriminator, Decl(discriminatedUnionIncompatibleMemberErrorMessage.ts, 6, 5))
16+
17+
type UnionType = "foo" | "bar" | "baz";
18+
>UnionType : Symbol(UnionType, Decl(discriminatedUnionIncompatibleMemberErrorMessage.ts, 6, 29))
19+
20+
const obj = { discriminator: "foo" as UnionType };
21+
>obj : Symbol(obj, Decl(discriminatedUnionIncompatibleMemberErrorMessage.ts, 10, 5))
22+
>discriminator : Symbol(discriminator, Decl(discriminatedUnionIncompatibleMemberErrorMessage.ts, 10, 13))
23+
>UnionType : Symbol(UnionType, Decl(discriminatedUnionIncompatibleMemberErrorMessage.ts, 6, 29))
24+
25+
const err: Discriminated = obj; // Error: "baz" is not present in discriminator
26+
>err : Symbol(err, Decl(discriminatedUnionIncompatibleMemberErrorMessage.ts, 11, 5))
27+
>Discriminated : Symbol(Discriminated, Decl(discriminatedUnionIncompatibleMemberErrorMessage.ts, 0, 0))
28+
>obj : Symbol(obj, Decl(discriminatedUnionIncompatibleMemberErrorMessage.ts, 10, 5))
29+
30+
// Multiple incompatible types
31+
type WiderUnion = "foo" | "bar" | "baz" | "qux";
32+
>WiderUnion : Symbol(WiderUnion, Decl(discriminatedUnionIncompatibleMemberErrorMessage.ts, 11, 31))
33+
34+
const obj2 = { discriminator: "foo" as WiderUnion };
35+
>obj2 : Symbol(obj2, Decl(discriminatedUnionIncompatibleMemberErrorMessage.ts, 15, 5))
36+
>discriminator : Symbol(discriminator, Decl(discriminatedUnionIncompatibleMemberErrorMessage.ts, 15, 14))
37+
>WiderUnion : Symbol(WiderUnion, Decl(discriminatedUnionIncompatibleMemberErrorMessage.ts, 11, 31))
38+
39+
const err2: Discriminated = obj2; // Error: "baz" | "qux" are not present in discriminator
40+
>err2 : Symbol(err2, Decl(discriminatedUnionIncompatibleMemberErrorMessage.ts, 16, 5))
41+
>Discriminated : Symbol(Discriminated, Decl(discriminatedUnionIncompatibleMemberErrorMessage.ts, 0, 0))
42+
>obj2 : Symbol(obj2, Decl(discriminatedUnionIncompatibleMemberErrorMessage.ts, 15, 5))
43+
44+
// Different discriminator property name
45+
type Shape =
46+
>Shape : Symbol(Shape, Decl(discriminatedUnionIncompatibleMemberErrorMessage.ts, 16, 33))
47+
48+
| { kind: "circle"; radius: number }
49+
>kind : Symbol(kind, Decl(discriminatedUnionIncompatibleMemberErrorMessage.ts, 20, 5))
50+
>radius : Symbol(radius, Decl(discriminatedUnionIncompatibleMemberErrorMessage.ts, 20, 21))
51+
52+
| { kind: "square"; side: number };
53+
>kind : Symbol(kind, Decl(discriminatedUnionIncompatibleMemberErrorMessage.ts, 21, 5))
54+
>side : Symbol(side, Decl(discriminatedUnionIncompatibleMemberErrorMessage.ts, 21, 21))
55+
56+
type ShapeKind = "circle" | "square" | "triangle";
57+
>ShapeKind : Symbol(ShapeKind, Decl(discriminatedUnionIncompatibleMemberErrorMessage.ts, 21, 37))
58+
59+
const shape = { kind: "circle" as ShapeKind };
60+
>shape : Symbol(shape, Decl(discriminatedUnionIncompatibleMemberErrorMessage.ts, 24, 5))
61+
>kind : Symbol(kind, Decl(discriminatedUnionIncompatibleMemberErrorMessage.ts, 24, 15))
62+
>ShapeKind : Symbol(ShapeKind, Decl(discriminatedUnionIncompatibleMemberErrorMessage.ts, 21, 37))
63+
64+
const err3: Shape = shape; // Error: "triangle" is not present in discriminator "kind"
65+
>err3 : Symbol(err3, Decl(discriminatedUnionIncompatibleMemberErrorMessage.ts, 25, 5))
66+
>Shape : Symbol(Shape, Decl(discriminatedUnionIncompatibleMemberErrorMessage.ts, 16, 33))
67+
>shape : Symbol(shape, Decl(discriminatedUnionIncompatibleMemberErrorMessage.ts, 24, 5))
68+
69+
// From issue #62603
70+
type Discriminated2 =
71+
>Discriminated2 : Symbol(Discriminated2, Decl(discriminatedUnionIncompatibleMemberErrorMessage.ts, 25, 26))
72+
73+
| { discriminator: "foo" }
74+
>discriminator : Symbol(discriminator, Decl(discriminatedUnionIncompatibleMemberErrorMessage.ts, 29, 5))
75+
76+
| { discriminator: "bar" };
77+
>discriminator : Symbol(discriminator, Decl(discriminatedUnionIncompatibleMemberErrorMessage.ts, 30, 5))
78+
79+
const unexpectedError: Discriminated2 = { discriminator: "foo" } as { discriminator: UnionType };
80+
>unexpectedError : Symbol(unexpectedError, Decl(discriminatedUnionIncompatibleMemberErrorMessage.ts, 32, 5))
81+
>Discriminated2 : Symbol(Discriminated2, Decl(discriminatedUnionIncompatibleMemberErrorMessage.ts, 25, 26))
82+
>discriminator : Symbol(discriminator, Decl(discriminatedUnionIncompatibleMemberErrorMessage.ts, 32, 41))
83+
>discriminator : Symbol(discriminator, Decl(discriminatedUnionIncompatibleMemberErrorMessage.ts, 32, 69))
84+
>UnionType : Symbol(UnionType, Decl(discriminatedUnionIncompatibleMemberErrorMessage.ts, 6, 29))
85+
86+
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
//// [tests/cases/compiler/discriminatedUnionIncompatibleMemberErrorMessage.ts] ////
2+
3+
=== discriminatedUnionIncompatibleMemberErrorMessage.ts ===
4+
// Test case for issue #62737
5+
// Improve error messages when assigning objects with wider discriminator unions to discriminated union types
6+
7+
// Basic case - single incompatible type
8+
type Discriminated =
9+
>Discriminated : Discriminated
10+
> : ^^^^^^^^^^^^^
11+
12+
| { discriminator: "foo" }
13+
>discriminator : "foo"
14+
> : ^^^^^
15+
16+
| { discriminator: "bar" };
17+
>discriminator : "bar"
18+
> : ^^^^^
19+
20+
type UnionType = "foo" | "bar" | "baz";
21+
>UnionType : UnionType
22+
> : ^^^^^^^^^
23+
24+
const obj = { discriminator: "foo" as UnionType };
25+
>obj : { discriminator: UnionType; }
26+
> : ^^^^^^^^^^^^^^^^^ ^^^
27+
>{ discriminator: "foo" as UnionType } : { discriminator: UnionType; }
28+
> : ^^^^^^^^^^^^^^^^^ ^^^
29+
>discriminator : UnionType
30+
> : ^^^^^^^^^
31+
>"foo" as UnionType : UnionType
32+
> : ^^^^^^^^^
33+
>"foo" : "foo"
34+
> : ^^^^^
35+
36+
const err: Discriminated = obj; // Error: "baz" is not present in discriminator
37+
>err : Discriminated
38+
> : ^^^^^^^^^^^^^
39+
>obj : { discriminator: UnionType; }
40+
> : ^^^^^^^^^^^^^^^^^ ^^^
41+
42+
// Multiple incompatible types
43+
type WiderUnion = "foo" | "bar" | "baz" | "qux";
44+
>WiderUnion : WiderUnion
45+
> : ^^^^^^^^^^
46+
47+
const obj2 = { discriminator: "foo" as WiderUnion };
48+
>obj2 : { discriminator: WiderUnion; }
49+
> : ^^^^^^^^^^^^^^^^^ ^^^
50+
>{ discriminator: "foo" as WiderUnion } : { discriminator: WiderUnion; }
51+
> : ^^^^^^^^^^^^^^^^^ ^^^
52+
>discriminator : WiderUnion
53+
> : ^^^^^^^^^^
54+
>"foo" as WiderUnion : WiderUnion
55+
> : ^^^^^^^^^^
56+
>"foo" : "foo"
57+
> : ^^^^^
58+
59+
const err2: Discriminated = obj2; // Error: "baz" | "qux" are not present in discriminator
60+
>err2 : Discriminated
61+
> : ^^^^^^^^^^^^^
62+
>obj2 : { discriminator: WiderUnion; }
63+
> : ^^^^^^^^^^^^^^^^^ ^^^
64+
65+
// Different discriminator property name
66+
type Shape =
67+
>Shape : Shape
68+
> : ^^^^^
69+
70+
| { kind: "circle"; radius: number }
71+
>kind : "circle"
72+
> : ^^^^^^^^
73+
>radius : number
74+
> : ^^^^^^
75+
76+
| { kind: "square"; side: number };
77+
>kind : "square"
78+
> : ^^^^^^^^
79+
>side : number
80+
> : ^^^^^^
81+
82+
type ShapeKind = "circle" | "square" | "triangle";
83+
>ShapeKind : ShapeKind
84+
> : ^^^^^^^^^
85+
86+
const shape = { kind: "circle" as ShapeKind };
87+
>shape : { kind: ShapeKind; }
88+
> : ^^^^^^^^ ^^^
89+
>{ kind: "circle" as ShapeKind } : { kind: ShapeKind; }
90+
> : ^^^^^^^^ ^^^
91+
>kind : ShapeKind
92+
> : ^^^^^^^^^
93+
>"circle" as ShapeKind : ShapeKind
94+
> : ^^^^^^^^^
95+
>"circle" : "circle"
96+
> : ^^^^^^^^
97+
98+
const err3: Shape = shape; // Error: "triangle" is not present in discriminator "kind"
99+
>err3 : Shape
100+
> : ^^^^^
101+
>shape : { kind: ShapeKind; }
102+
> : ^^^^^^^^ ^^^
103+
104+
// From issue #62603
105+
type Discriminated2 =
106+
>Discriminated2 : Discriminated2
107+
> : ^^^^^^^^^^^^^^
108+
109+
| { discriminator: "foo" }
110+
>discriminator : "foo"
111+
> : ^^^^^
112+
113+
| { discriminator: "bar" };
114+
>discriminator : "bar"
115+
> : ^^^^^
116+
117+
const unexpectedError: Discriminated2 = { discriminator: "foo" } as { discriminator: UnionType };
118+
>unexpectedError : Discriminated2
119+
> : ^^^^^^^^^^^^^^
120+
>{ discriminator: "foo" } as { discriminator: UnionType } : { discriminator: UnionType; }
121+
> : ^^^^^^^^^^^^^^^^^ ^^^
122+
>{ discriminator: "foo" } : { discriminator: "foo"; }
123+
> : ^^^^^^^^^^^^^^^^^^^^^^^^^
124+
>discriminator : "foo"
125+
> : ^^^^^
126+
>"foo" : "foo"
127+
> : ^^^^^
128+
>discriminator : UnionType
129+
> : ^^^^^^^^^
130+
131+

0 commit comments

Comments
 (0)