Skip to content

Commit 19d89c4

Browse files
authored
Merge pull request #10296 from Microsoft/fixDiscriminantWithPrimtive
Fix discriminated unions with primtive types
2 parents b988bc9 + e64675e commit 19d89c4

File tree

6 files changed

+471
-61
lines changed

6 files changed

+471
-61
lines changed

src/compiler/checker.ts

Lines changed: 62 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -245,7 +245,8 @@ namespace ts {
245245
NEUndefinedOrNull = 1 << 19, // x != undefined / x != null
246246
Truthy = 1 << 20, // x
247247
Falsy = 1 << 21, // !x
248-
All = (1 << 22) - 1,
248+
Discriminatable = 1 << 22, // May have discriminant property
249+
All = (1 << 23) - 1,
249250
// The following members encode facts about particular kinds of types for use in the getTypeFacts function.
250251
// The presence of a particular fact means that the given test is true for some (and possibly all) values
251252
// of that kind of type.
@@ -275,9 +276,9 @@ namespace ts {
275276
TrueFacts = BaseBooleanFacts | Truthy,
276277
SymbolStrictFacts = TypeofEQSymbol | TypeofNEString | TypeofNENumber | TypeofNEBoolean | TypeofNEObject | TypeofNEFunction | TypeofNEHostObject | NEUndefined | NENull | NEUndefinedOrNull | Truthy,
277278
SymbolFacts = SymbolStrictFacts | EQUndefined | EQNull | EQUndefinedOrNull | Falsy,
278-
ObjectStrictFacts = TypeofEQObject | TypeofEQHostObject | TypeofNEString | TypeofNENumber | TypeofNEBoolean | TypeofNESymbol | TypeofNEFunction | NEUndefined | NENull | NEUndefinedOrNull | Truthy,
279+
ObjectStrictFacts = TypeofEQObject | TypeofEQHostObject | TypeofNEString | TypeofNENumber | TypeofNEBoolean | TypeofNESymbol | TypeofNEFunction | NEUndefined | NENull | NEUndefinedOrNull | Truthy | Discriminatable,
279280
ObjectFacts = ObjectStrictFacts | EQUndefined | EQNull | EQUndefinedOrNull | Falsy,
280-
FunctionStrictFacts = TypeofEQFunction | TypeofEQHostObject | TypeofNEString | TypeofNENumber | TypeofNEBoolean | TypeofNESymbol | TypeofNEObject | NEUndefined | NENull | NEUndefinedOrNull | Truthy,
281+
FunctionStrictFacts = TypeofEQFunction | TypeofEQHostObject | TypeofNEString | TypeofNENumber | TypeofNEBoolean | TypeofNESymbol | TypeofNEObject | NEUndefined | NENull | NEUndefinedOrNull | Truthy | Discriminatable,
281282
FunctionFacts = FunctionStrictFacts | EQUndefined | EQNull | EQUndefinedOrNull | Falsy,
282283
UndefinedFacts = TypeofNEString | TypeofNENumber | TypeofNEBoolean | TypeofNESymbol | TypeofNEObject | TypeofNEFunction | TypeofNEHostObject | EQUndefined | EQUndefinedOrNull | NENull | Falsy,
283284
NullFacts = TypeofEQObject | TypeofNEString | TypeofNENumber | TypeofNEBoolean | TypeofNESymbol | TypeofNEFunction | TypeofNEHostObject | EQNull | EQUndefinedOrNull | NEUndefined | Falsy,
@@ -5351,12 +5352,12 @@ namespace ts {
53515352
}
53525353
}
53535354

5354-
// We deduplicate the constituent types based on object identity. If the subtypeReduction flag is
5355-
// specified we also reduce the constituent type set to only include types that aren't subtypes of
5356-
// other types. Subtype reduction is expensive for large union types and is possible only when union
5355+
// We sort and deduplicate the constituent types based on object identity. If the subtypeReduction
5356+
// flag is specified we also reduce the constituent type set to only include types that aren't subtypes
5357+
// of other types. Subtype reduction is expensive for large union types and is possible only when union
53575358
// types are known not to circularly reference themselves (as is the case with union types created by
53585359
// expression constructs such as array literals and the || and ?: operators). Named types can
5359-
// circularly reference themselves and therefore cannot be deduplicated during their declaration.
5360+
// circularly reference themselves and therefore cannot be subtype reduced during their declaration.
53605361
// For example, "type Item = string | (() => Item" is a named type that circularly references itself.
53615362
function getUnionType(types: Type[], subtypeReduction?: boolean, aliasSymbol?: Symbol, aliasTypeArguments?: Type[]): Type {
53625363
if (types.length === 0) {
@@ -5378,15 +5379,23 @@ namespace ts {
53785379
typeSet.containsUndefined ? typeSet.containsNonWideningType ? undefinedType : undefinedWideningType :
53795380
neverType;
53805381
}
5381-
else if (typeSet.length === 1) {
5382-
return typeSet[0];
5382+
return getUnionTypeFromSortedList(typeSet, aliasSymbol, aliasTypeArguments);
5383+
}
5384+
5385+
// This function assumes the constituent type list is sorted and deduplicated.
5386+
function getUnionTypeFromSortedList(types: Type[], aliasSymbol?: Symbol, aliasTypeArguments?: Type[]): Type {
5387+
if (types.length === 0) {
5388+
return neverType;
53835389
}
5384-
const id = getTypeListId(typeSet);
5390+
if (types.length === 1) {
5391+
return types[0];
5392+
}
5393+
const id = getTypeListId(types);
53855394
let type = unionTypes[id];
53865395
if (!type) {
5387-
const propagatedFlags = getPropagatingFlagsOfTypes(typeSet, /*excludeKinds*/ TypeFlags.Nullable);
5396+
const propagatedFlags = getPropagatingFlagsOfTypes(types, /*excludeKinds*/ TypeFlags.Nullable);
53885397
type = unionTypes[id] = <UnionType>createObjectType(TypeFlags.Union | propagatedFlags);
5389-
type.types = typeSet;
5398+
type.types = types;
53905399
type.aliasSymbol = aliasSymbol;
53915400
type.aliasTypeArguments = aliasTypeArguments;
53925401
}
@@ -7848,17 +7857,24 @@ namespace ts {
78487857
}
78497858

78507859
function isDiscriminantProperty(type: Type, name: string) {
7851-
if (type) {
7852-
const nonNullType = getNonNullableType(type);
7853-
if (nonNullType.flags & TypeFlags.Union) {
7854-
const prop = getPropertyOfType(nonNullType, name);
7855-
if (prop && prop.flags & SymbolFlags.SyntheticProperty) {
7856-
if ((<TransientSymbol>prop).isDiscriminantProperty === undefined) {
7857-
(<TransientSymbol>prop).isDiscriminantProperty = !(<TransientSymbol>prop).hasCommonType &&
7858-
isUnitUnionType(getTypeOfSymbol(prop));
7859-
}
7860-
return (<TransientSymbol>prop).isDiscriminantProperty;
7860+
if (type && type.flags & TypeFlags.Union) {
7861+
let prop = getPropertyOfType(type, name);
7862+
if (!prop) {
7863+
// The type may be a union that includes nullable or primitive types. If filtering
7864+
// those out produces a different type, get the property from that type instead.
7865+
// Effectively, we're checking if this *could* be a discriminant property once nullable
7866+
// and primitive types are removed by other type guards.
7867+
const filteredType = getTypeWithFacts(type, TypeFacts.Discriminatable);
7868+
if (filteredType !== type && filteredType.flags & TypeFlags.Union) {
7869+
prop = getPropertyOfType(filteredType, name);
7870+
}
7871+
}
7872+
if (prop && prop.flags & SymbolFlags.SyntheticProperty) {
7873+
if ((<TransientSymbol>prop).isDiscriminantProperty === undefined) {
7874+
(<TransientSymbol>prop).isDiscriminantProperty = !(<TransientSymbol>prop).hasCommonType &&
7875+
isUnitUnionType(getTypeOfSymbol(prop));
78617876
}
7877+
return (<TransientSymbol>prop).isDiscriminantProperty;
78627878
}
78637879
}
78647880
return false;
@@ -7907,10 +7923,10 @@ namespace ts {
79077923
// For example, when a variable of type number | string | boolean is assigned a value of type number | boolean,
79087924
// we remove type string.
79097925
function getAssignmentReducedType(declaredType: UnionType, assignedType: Type) {
7910-
if (declaredType !== assignedType && declaredType.flags & TypeFlags.Union) {
7911-
const reducedTypes = filter(declaredType.types, t => typeMaybeAssignableTo(assignedType, t));
7912-
if (reducedTypes.length) {
7913-
return reducedTypes.length === 1 ? reducedTypes[0] : getUnionType(reducedTypes);
7926+
if (declaredType !== assignedType) {
7927+
const reducedType = filterType(declaredType, t => typeMaybeAssignableTo(assignedType, t));
7928+
if (reducedType !== neverType) {
7929+
return reducedType;
79147930
}
79157931
}
79167932
return declaredType;
@@ -7924,6 +7940,14 @@ namespace ts {
79247940
return result;
79257941
}
79267942

7943+
function isFunctionObjectType(type: ObjectType): boolean {
7944+
// We do a quick check for a "bind" property before performing the more expensive subtype
7945+
// check. This gives us a quicker out in the common case where an object type is not a function.
7946+
const resolved = resolveStructuredTypeMembers(type);
7947+
return !!(resolved.callSignatures.length || resolved.constructSignatures.length ||
7948+
hasProperty(resolved.members, "bind") && isTypeSubtypeOf(type, globalFunctionType));
7949+
}
7950+
79277951
function getTypeFacts(type: Type): TypeFacts {
79287952
const flags = type.flags;
79297953
if (flags & TypeFlags.String) {
@@ -7952,8 +7976,7 @@ namespace ts {
79527976
type === falseType ? TypeFacts.FalseFacts : TypeFacts.TrueFacts;
79537977
}
79547978
if (flags & TypeFlags.ObjectType) {
7955-
const resolved = resolveStructuredTypeMembers(type);
7956-
return resolved.callSignatures.length || resolved.constructSignatures.length || isTypeSubtypeOf(type, globalFunctionType) ?
7979+
return isFunctionObjectType(<ObjectType>type) ?
79577980
strictNullChecks ? TypeFacts.FunctionStrictFacts : TypeFacts.FunctionFacts :
79587981
strictNullChecks ? TypeFacts.ObjectStrictFacts : TypeFacts.ObjectFacts;
79597982
}
@@ -7977,26 +8000,7 @@ namespace ts {
79778000
}
79788001

79798002
function getTypeWithFacts(type: Type, include: TypeFacts) {
7980-
if (!(type.flags & TypeFlags.Union)) {
7981-
return getTypeFacts(type) & include ? type : neverType;
7982-
}
7983-
let firstType: Type;
7984-
let types: Type[];
7985-
for (const t of (type as UnionType).types) {
7986-
if (getTypeFacts(t) & include) {
7987-
if (!firstType) {
7988-
firstType = t;
7989-
}
7990-
else {
7991-
if (!types) {
7992-
types = [firstType];
7993-
}
7994-
types.push(t);
7995-
}
7996-
}
7997-
}
7998-
return types ? getUnionType(types) :
7999-
firstType ? firstType : neverType;
8003+
return filterType(type, t => (getTypeFacts(t) & include) !== 0);
80008004
}
80018005

80028006
function getTypeWithDefault(type: Type, defaultExpression: Expression) {
@@ -8172,9 +8176,12 @@ namespace ts {
81728176
}
81738177

81748178
function filterType(type: Type, f: (t: Type) => boolean): Type {
8175-
return type.flags & TypeFlags.Union ?
8176-
getUnionType(filter((<UnionType>type).types, f)) :
8177-
f(type) ? type : neverType;
8179+
if (type.flags & TypeFlags.Union) {
8180+
const types = (<UnionType>type).types;
8181+
const filtered = filter(types, f);
8182+
return filtered === types ? type : getUnionTypeFromSortedList(filtered);
8183+
}
8184+
return f(type) ? type : neverType;
81788185
}
81798186

81808187
function isIncomplete(flowType: FlowType) {
@@ -8512,7 +8519,7 @@ namespace ts {
85128519
}
85138520
if (assumeTrue && !(type.flags & TypeFlags.Union)) {
85148521
// We narrow a non-union type to an exact primitive type if the non-union type
8515-
// is a supertype of that primtive type. For example, type 'any' can be narrowed
8522+
// is a supertype of that primitive type. For example, type 'any' can be narrowed
85168523
// to one of the primitive types.
85178524
const targetType = getProperty(typeofTypesByName, literal.text);
85188525
if (targetType && isTypeSubtypeOf(targetType, type)) {
@@ -8601,9 +8608,9 @@ namespace ts {
86018608
// If the current type is a union type, remove all constituents that couldn't be instances of
86028609
// the candidate type. If one or more constituents remain, return a union of those.
86038610
if (type.flags & TypeFlags.Union) {
8604-
const assignableConstituents = filter((<UnionType>type).types, t => isTypeInstanceOf(t, candidate));
8605-
if (assignableConstituents.length) {
8606-
return getUnionType(assignableConstituents);
8611+
const assignableType = filterType(type, t => isTypeInstanceOf(t, candidate));
8612+
if (assignableType !== neverType) {
8613+
return assignableType;
86078614
}
86088615
}
86098616
// If the candidate type is a subtype of the target type, narrow to the candidate type.

src/compiler/core.ts

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -147,17 +147,29 @@ namespace ts {
147147
return count;
148148
}
149149

150+
/**
151+
* Filters an array by a predicate function. Returns the same array instance if the predicate is
152+
* true for all elements, otherwise returns a new array instance containing the filtered subset.
153+
*/
150154
export function filter<T>(array: T[], f: (x: T) => boolean): T[] {
151-
let result: T[];
152155
if (array) {
153-
result = [];
154-
for (const item of array) {
155-
if (f(item)) {
156-
result.push(item);
156+
const len = array.length;
157+
let i = 0;
158+
while (i < len && f(array[i])) i++;
159+
if (i < len) {
160+
const result = array.slice(0, i);
161+
i++;
162+
while (i < len) {
163+
const item = array[i];
164+
if (f(item)) {
165+
result.push(item);
166+
}
167+
i++;
157168
}
169+
return result;
158170
}
159171
}
160-
return result;
172+
return array;
161173
}
162174

163175
export function filterMutate<T>(array: T[], f: (x: T) => boolean): void {
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
//// [discriminantsAndPrimitives.ts]
2+
3+
// Repro from #10257 plus other tests
4+
5+
interface Foo {
6+
kind: "foo";
7+
name: string;
8+
}
9+
10+
interface Bar {
11+
kind: "bar";
12+
length: string;
13+
}
14+
15+
function f1(x: Foo | Bar | string) {
16+
if (typeof x !== 'string') {
17+
switch(x.kind) {
18+
case 'foo':
19+
x.name;
20+
}
21+
}
22+
}
23+
24+
function f2(x: Foo | Bar | string | undefined) {
25+
if (typeof x === "object") {
26+
switch(x.kind) {
27+
case 'foo':
28+
x.name;
29+
}
30+
}
31+
}
32+
33+
function f3(x: Foo | Bar | string | null) {
34+
if (x && typeof x !== "string") {
35+
switch(x.kind) {
36+
case 'foo':
37+
x.name;
38+
}
39+
}
40+
}
41+
42+
function f4(x: Foo | Bar | string | number | null) {
43+
if (x && typeof x === "object") {
44+
switch(x.kind) {
45+
case 'foo':
46+
x.name;
47+
}
48+
}
49+
}
50+
51+
//// [discriminantsAndPrimitives.js]
52+
// Repro from #10257 plus other tests
53+
function f1(x) {
54+
if (typeof x !== 'string') {
55+
switch (x.kind) {
56+
case 'foo':
57+
x.name;
58+
}
59+
}
60+
}
61+
function f2(x) {
62+
if (typeof x === "object") {
63+
switch (x.kind) {
64+
case 'foo':
65+
x.name;
66+
}
67+
}
68+
}
69+
function f3(x) {
70+
if (x && typeof x !== "string") {
71+
switch (x.kind) {
72+
case 'foo':
73+
x.name;
74+
}
75+
}
76+
}
77+
function f4(x) {
78+
if (x && typeof x === "object") {
79+
switch (x.kind) {
80+
case 'foo':
81+
x.name;
82+
}
83+
}
84+
}

0 commit comments

Comments
 (0)