Skip to content

Commit a9ad94a

Browse files
authored
Conditional type simplifications & Globally cached conditional type instances (#29437)
* Introduce simpliciations for extract/exclude-like conditional types and fix restrictive instantiations * Add test for the common simplifications * unify true branch constraint generation logic and true branch simplification * Use identical check on instantiated types * Add late-instantiate conditionals to test * Globally cache conditional type instantiations ala indexed access types * Handle `any` simplifications * Factor empty intersection check into function * Modifify conditional type constraints to better handle single-branch `any` and restrictive type parameters * Add test case motivating prior commit * Fix lint * Factor logic into worker vs cacheing function * Remove now unneeded casts
1 parent 6607e00 commit a9ad94a

12 files changed

+1554
-27
lines changed

src/compiler/checker.ts

+84-13
Original file line numberDiff line numberDiff line change
@@ -387,6 +387,7 @@ namespace ts {
387387
const intersectionTypes = createMap<IntersectionType>();
388388
const literalTypes = createMap<LiteralType>();
389389
const indexedAccessTypes = createMap<IndexedAccessType>();
390+
const conditionalTypes = createMap<Type>();
390391
const evolvingArrayTypes: EvolvingArrayType[] = [];
391392
const undefinedProperties = createMap<Symbol>() as UnderscoreEscapedMap<Symbol>;
392393

@@ -7461,15 +7462,25 @@ namespace ts {
74617462
return baseConstraint && baseConstraint !== type ? baseConstraint : undefined;
74627463
}
74637464

7465+
function getDefaultConstraintOfTrueBranchOfConditionalType(root: ConditionalRoot, combinedMapper: TypeMapper | undefined, mapper: TypeMapper | undefined) {
7466+
const rootTrueType = root.trueType;
7467+
const rootTrueConstraint = !(rootTrueType.flags & TypeFlags.Substitution)
7468+
? rootTrueType
7469+
: instantiateType(((<SubstitutionType>rootTrueType).substitute), combinedMapper || mapper).flags & TypeFlags.AnyOrUnknown
7470+
? (<SubstitutionType>rootTrueType).typeVariable
7471+
: getIntersectionType([(<SubstitutionType>rootTrueType).substitute, (<SubstitutionType>rootTrueType).typeVariable]);
7472+
return instantiateType(rootTrueConstraint, combinedMapper || mapper);
7473+
}
7474+
74647475
function getDefaultConstraintOfConditionalType(type: ConditionalType) {
74657476
if (!type.resolvedDefaultConstraint) {
7466-
const rootTrueType = type.root.trueType;
7467-
const rootTrueConstraint = !(rootTrueType.flags & TypeFlags.Substitution)
7468-
? rootTrueType
7469-
: ((<SubstitutionType>rootTrueType).substitute).flags & TypeFlags.AnyOrUnknown
7470-
? (<SubstitutionType>rootTrueType).typeVariable
7471-
: getIntersectionType([(<SubstitutionType>rootTrueType).substitute, (<SubstitutionType>rootTrueType).typeVariable]);
7472-
type.resolvedDefaultConstraint = getUnionType([instantiateType(rootTrueConstraint, type.combinedMapper || type.mapper), getFalseTypeFromConditionalType(type)]);
7477+
// An `any` branch of a conditional type would normally be viral - specifically, without special handling here,
7478+
// a conditional type with a single branch of type `any` would be assignable to anything, since it's constraint would simplify to
7479+
// just `any`. This result is _usually_ unwanted - so instead here we elide an `any` branch from the constraint type,
7480+
// in effect treating `any` like `never` rather than `unknown` in this location.
7481+
const trueConstraint = getDefaultConstraintOfTrueBranchOfConditionalType(type.root, type.combinedMapper, type.mapper);
7482+
const falseConstraint = getFalseTypeFromConditionalType(type);
7483+
type.resolvedDefaultConstraint = isTypeAny(trueConstraint) ? falseConstraint : isTypeAny(falseConstraint) ? trueConstraint : getUnionType([trueConstraint, falseConstraint]);
74737484
}
74747485
return type.resolvedDefaultConstraint;
74757486
}
@@ -7480,7 +7491,13 @@ namespace ts {
74807491
// with its constraint. We do this because if the constraint is a union type it will be distributed
74817492
// over the conditional type and possibly reduced. For example, 'T extends undefined ? never : T'
74827493
// removes 'undefined' from T.
7483-
if (type.root.isDistributive) {
7494+
// We skip returning a distributive constraint for a restrictive instantiation of a conditional type
7495+
// as the constraint for all type params (check type included) have been replace with `unknown`, which
7496+
// is going to produce even more false positive/negative results than the distribute constraint already does.
7497+
// Please note: the distributive constraint is a kludge for emulating what a negated type could to do filter
7498+
// a union - once negated types exist and are applied to the conditional false branch, this "constraint"
7499+
// likely doesn't need to exist.
7500+
if (type.root.isDistributive && type.restrictiveInstantiation !== type) {
74847501
const simplified = getSimplifiedType(type.checkType);
74857502
const constraint = simplified === type.checkType ? getConstraintOfType(simplified) : simplified;
74867503
if (constraint && constraint !== type.checkType) {
@@ -10089,12 +10106,50 @@ namespace ts {
1008910106
return type.flags & TypeFlags.Substitution ? (<SubstitutionType>type).typeVariable : type;
1009010107
}
1009110108

10109+
/**
10110+
* Invokes union simplification logic to determine if an intersection is considered empty as a union constituent
10111+
*/
10112+
function isIntersectionEmpty(type1: Type, type2: Type) {
10113+
return !!(getUnionType([intersectTypes(type1, type2), neverType]).flags & TypeFlags.Never);
10114+
}
10115+
1009210116
function getConditionalType(root: ConditionalRoot, mapper: TypeMapper | undefined): Type {
1009310117
const checkType = instantiateType(root.checkType, mapper);
1009410118
const extendsType = instantiateType(root.extendsType, mapper);
1009510119
if (checkType === wildcardType || extendsType === wildcardType) {
1009610120
return wildcardType;
1009710121
}
10122+
const trueType = instantiateType(root.trueType, mapper);
10123+
const falseType = instantiateType(root.falseType, mapper);
10124+
const instantiationId = `${root.isDistributive ? "d" : ""}${getTypeId(checkType)}>${getTypeId(extendsType)}?${getTypeId(trueType)}:${getTypeId(falseType)}`;
10125+
const result = conditionalTypes.get(instantiationId);
10126+
if (result) {
10127+
return result;
10128+
}
10129+
const newResult = getConditionalTypeWorker(root, mapper, checkType, extendsType, trueType, falseType);
10130+
conditionalTypes.set(instantiationId, newResult);
10131+
return newResult;
10132+
}
10133+
10134+
function getConditionalTypeWorker(root: ConditionalRoot, mapper: TypeMapper | undefined, checkType: Type, extendsType: Type, trueType: Type, falseType: Type) {
10135+
// Simplifications for types of the form `T extends U ? T : never` and `T extends U ? never : T`.
10136+
if (falseType.flags & TypeFlags.Never && isTypeIdenticalTo(getActualTypeVariable(trueType), getActualTypeVariable(checkType))) {
10137+
if (checkType.flags & TypeFlags.Any || isTypeAssignableTo(getRestrictiveInstantiation(checkType), getRestrictiveInstantiation(extendsType))) { // Always true
10138+
return getDefaultConstraintOfTrueBranchOfConditionalType(root, /*combinedMapper*/ undefined, mapper);
10139+
}
10140+
else if (isIntersectionEmpty(checkType, extendsType)) { // Always false
10141+
return neverType;
10142+
}
10143+
}
10144+
else if (trueType.flags & TypeFlags.Never && isTypeIdenticalTo(getActualTypeVariable(falseType), getActualTypeVariable(checkType))) {
10145+
if (!(checkType.flags & TypeFlags.Any) && isTypeAssignableTo(getRestrictiveInstantiation(checkType), getRestrictiveInstantiation(extendsType))) { // Always true
10146+
return neverType;
10147+
}
10148+
else if (checkType.flags & TypeFlags.Any || isIntersectionEmpty(checkType, extendsType)) { // Always false
10149+
return falseType; // TODO: Intersect negated `extends` type here
10150+
}
10151+
}
10152+
1009810153
const checkTypeInstantiable = maybeTypeOfKind(checkType, TypeFlags.Instantiable | TypeFlags.GenericMappedType);
1009910154
let combinedMapper: TypeMapper | undefined;
1010010155
if (root.inferTypeParameters) {
@@ -10112,18 +10167,18 @@ namespace ts {
1011210167
// We attempt to resolve the conditional type only when the check and extends types are non-generic
1011310168
if (!checkTypeInstantiable && !maybeTypeOfKind(inferredExtendsType, TypeFlags.Instantiable | TypeFlags.GenericMappedType)) {
1011410169
if (inferredExtendsType.flags & TypeFlags.AnyOrUnknown) {
10115-
return instantiateType(root.trueType, mapper);
10170+
return trueType;
1011610171
}
1011710172
// Return union of trueType and falseType for 'any' since it matches anything
1011810173
if (checkType.flags & TypeFlags.Any) {
10119-
return getUnionType([instantiateType(root.trueType, combinedMapper || mapper), instantiateType(root.falseType, mapper)]);
10174+
return getUnionType([instantiateType(root.trueType, combinedMapper || mapper), falseType]);
1012010175
}
1012110176
// Return falseType for a definitely false extends check. We check an instantiations of the two
1012210177
// types with type parameters mapped to the wildcard type, the most permissive instantiations
1012310178
// possible (the wildcard type is assignable to and from all types). If those are not related,
1012410179
// then no instantiations will be and we can just return the false branch type.
1012510180
if (!isTypeAssignableTo(getPermissiveInstantiation(checkType), getPermissiveInstantiation(inferredExtendsType))) {
10126-
return instantiateType(root.falseType, mapper);
10181+
return falseType;
1012710182
}
1012810183
// Return trueType for a definitely true extends check. We check instantiations of the two
1012910184
// types with type parameters mapped to their restrictive form, i.e. a form of the type parameter
@@ -10142,6 +10197,10 @@ namespace ts {
1014210197
result.extendsType = extendsType;
1014310198
result.mapper = mapper;
1014410199
result.combinedMapper = combinedMapper;
10200+
if (!combinedMapper) {
10201+
result.resolvedTrueType = trueType;
10202+
result.resolvedFalseType = falseType;
10203+
}
1014510204
result.aliasSymbol = root.aliasSymbol;
1014610205
result.aliasTypeArguments = instantiateTypes(root.aliasTypeArguments, mapper!); // TODO: GH#18217
1014710206
return result;
@@ -11132,8 +11191,20 @@ namespace ts {
1113211191
}
1113311192

1113411193
function getRestrictiveInstantiation(type: Type) {
11135-
return type.flags & (TypeFlags.Primitive | TypeFlags.AnyOrUnknown | TypeFlags.Never) ? type :
11136-
type.restrictiveInstantiation || (type.restrictiveInstantiation = instantiateType(type, restrictiveMapper));
11194+
if (type.flags & (TypeFlags.Primitive | TypeFlags.AnyOrUnknown | TypeFlags.Never)) {
11195+
return type;
11196+
}
11197+
if (type.restrictiveInstantiation) {
11198+
return type.restrictiveInstantiation;
11199+
}
11200+
type.restrictiveInstantiation = instantiateType(type, restrictiveMapper);
11201+
// We set the following so we don't attempt to set the restrictive instance of a restrictive instance
11202+
// which is redundant - we'll produce new type identities, but all type params have already been mapped.
11203+
// This also gives us a way to detect restrictive instances upon comparisons and _disable_ the "distributeive constraint"
11204+
// assignability check for them, which is distinctly unsafe, as once you have a restrctive instance, all the type parameters
11205+
// are constrained to `unknown` and produce tons of false positives/negatives!
11206+
type.restrictiveInstantiation.restrictiveInstantiation = type.restrictiveInstantiation;
11207+
return type.restrictiveInstantiation;
1113711208
}
1113811209

1113911210
function instantiateIndexInfo(info: IndexInfo | undefined, mapper: TypeMapper): IndexInfo | undefined {

tests/baselines/reference/conditionalTypes1.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -509,6 +509,8 @@ declare function f5<T extends Options, K extends string>(p: K): Extract<T, {
509509
}>;
510510
declare let x0: {
511511
k: "a";
512+
} & {
513+
k: "a";
512514
a: number;
513515
};
514516
declare type OptionsOfKind<K extends Options["k"]> = Extract<Options, {
@@ -645,7 +647,7 @@ declare type T82 = Eq2<false, true>;
645647
declare type T83 = Eq2<false, false>;
646648
declare type Foo<T> = T extends string ? boolean : number;
647649
declare type Bar<T> = T extends string ? boolean : number;
648-
declare const convert: <U>(value: Foo<U>) => Bar<U>;
650+
declare const convert: <U>(value: Foo<U>) => Foo<U>;
649651
declare type Baz<T> = Foo<T>;
650652
declare const convert2: <T>(value: Foo<T>) => Foo<T>;
651653
declare function f31<T>(): void;

tests/baselines/reference/conditionalTypes1.types

+10-10
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ type T02 = Exclude<string | number | (() => void), Function>; // string | numbe
99
>T02 : string | number
1010

1111
type T03 = Extract<string | number | (() => void), Function>; // () => void
12-
>T03 : () => void
12+
>T03 : Function & (() => void)
1313

1414
type T04 = NonNullable<string | number | undefined>; // string | number
1515
>T04 : string | number
@@ -113,7 +113,7 @@ type T10 = Exclude<Options, { k: "a" | "b" }>; // { k: "c", c: boolean }
113113
>k : "a" | "b"
114114

115115
type T11 = Extract<Options, { k: "a" | "b" }>; // { k: "a", a: number } | { k: "b", b: string }
116-
>T11 : { k: "a"; a: number; } | { k: "b"; b: string; }
116+
>T11 : ({ k: "a" | "b"; } & { k: "a"; a: number; }) | ({ k: "a" | "b"; } & { k: "b"; b: string; })
117117
>k : "a" | "b"
118118

119119
type T12 = Exclude<Options, { k: "a" } | { k: "b" }>; // { k: "c", c: boolean }
@@ -122,7 +122,7 @@ type T12 = Exclude<Options, { k: "a" } | { k: "b" }>; // { k: "c", c: boolean }
122122
>k : "b"
123123

124124
type T13 = Extract<Options, { k: "a" } | { k: "b" }>; // { k: "a", a: number } | { k: "b", b: string }
125-
>T13 : { k: "a"; a: number; } | { k: "b"; b: string; }
125+
>T13 : ({ k: "a"; } & { k: "a"; a: number; }) | ({ k: "b"; } & { k: "a"; a: number; }) | ({ k: "a"; } & { k: "b"; b: string; }) | ({ k: "b"; } & { k: "b"; b: string; })
126126
>k : "a"
127127
>k : "b"
128128

@@ -140,8 +140,8 @@ declare function f5<T extends Options, K extends string>(p: K): Extract<T, { k:
140140
>k : K
141141

142142
let x0 = f5("a"); // { k: "a", a: number }
143-
>x0 : { k: "a"; a: number; }
144-
>f5("a") : { k: "a"; a: number; }
143+
>x0 : { k: "a"; } & { k: "a"; a: number; }
144+
>f5("a") : { k: "a"; } & { k: "a"; a: number; }
145145
>f5 : <T extends Options, K extends string>(p: K) => Extract<T, { k: K; }>
146146
>"a" : "a"
147147

@@ -150,13 +150,13 @@ type OptionsOfKind<K extends Options["k"]> = Extract<Options, { k: K }>;
150150
>k : K
151151

152152
type T16 = OptionsOfKind<"a" | "b">; // { k: "a", a: number } | { k: "b", b: string }
153-
>T16 : { k: "a"; a: number; } | { k: "b"; b: string; }
153+
>T16 : ({ k: "a" | "b"; } & { k: "a"; a: number; }) | ({ k: "a" | "b"; } & { k: "b"; b: string; })
154154

155155
type Select<T, K extends keyof T, V extends T[K]> = Extract<T, { [P in K]: V }>;
156156
>Select : Extract<T, { [P in K]: V; }>
157157

158158
type T17 = Select<Options, "k", "a" | "b">; // // { k: "a", a: number } | { k: "b", b: string }
159-
>T17 : { k: "a"; a: number; } | { k: "b"; b: string; }
159+
>T17 : ({ k: "a" | "b"; } & { k: "a"; a: number; }) | ({ k: "a" | "b"; } & { k: "b"; b: string; })
160160

161161
type TypeName<T> =
162162
>TypeName : TypeName<T>
@@ -779,8 +779,8 @@ type Bar<T> = T extends string ? boolean : number;
779779
>Bar : Bar<T>
780780

781781
const convert = <U>(value: Foo<U>): Bar<U> => value;
782-
>convert : <U>(value: Foo<U>) => Bar<U>
783-
><U>(value: Foo<U>): Bar<U> => value : <U>(value: Foo<U>) => Bar<U>
782+
>convert : <U>(value: Foo<U>) => Foo<U>
783+
><U>(value: Foo<U>): Bar<U> => value : <U>(value: Foo<U>) => Foo<U>
784784
>value : Foo<U>
785785
>value : Foo<U>
786786

@@ -832,7 +832,7 @@ function f33<T, U>() {
832832
>T1 : Foo<T & U>
833833

834834
type T2 = Bar<T & U>;
835-
>T2 : Bar<T & U>
835+
>T2 : Foo<T & U>
836836

837837
var z: T1;
838838
>z : Foo<T & U>

tests/baselines/reference/conditionalTypes2.types

+3-3
Original file line numberDiff line numberDiff line change
@@ -130,14 +130,14 @@ function f12(x: string | (() => string) | undefined) {
130130
>x : string | (() => string) | undefined
131131

132132
const f = getFunction(x); // () => string
133-
>f : () => string
134-
>getFunction(x) : () => string
133+
>f : Function & (() => string)
134+
>getFunction(x) : Function & (() => string)
135135
>getFunction : <T>(item: T) => Extract<T, Function>
136136
>x : string | (() => string) | undefined
137137

138138
f();
139139
>f() : string
140-
>f : () => string
140+
>f : Function & (() => string)
141141
}
142142

143143
type Foo = { foo: string };
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
//// [conditionalTypesSimplifyWhenTrivial.ts]
2+
const fn1 = <Params>(
3+
params: Pick<Params, Exclude<keyof Params, never>>,
4+
): Params => params;
5+
6+
function fn2<T>(x: Exclude<T, never>) {
7+
var y: T = x;
8+
x = y;
9+
}
10+
11+
const fn3 = <Params>(
12+
params: Pick<Params, Extract<keyof Params, keyof Params>>,
13+
): Params => params;
14+
15+
function fn4<T>(x: Extract<T, T>) {
16+
var y: T = x;
17+
x = y;
18+
}
19+
20+
declare var x: Extract<number | string, any>; // Should be `numebr | string` and not `any`
21+
22+
type ExtractWithDefault<T, U, D = never> = T extends U ? T : D;
23+
24+
type ExcludeWithDefault<T, U, D = never> = T extends U ? D : T;
25+
26+
const fn5 = <Params>(
27+
params: Pick<Params, ExcludeWithDefault<keyof Params, never>>,
28+
): Params => params;
29+
30+
function fn6<T>(x: ExcludeWithDefault<T, never>) {
31+
var y: T = x;
32+
x = y;
33+
}
34+
35+
const fn7 = <Params>(
36+
params: Pick<Params, ExtractWithDefault<keyof Params, keyof Params>>,
37+
): Params => params;
38+
39+
function fn8<T>(x: ExtractWithDefault<T, T>) {
40+
var y: T = x;
41+
x = y;
42+
}
43+
44+
type TemplatedConditional<TCheck, TExtends, TTrue, TFalse> = TCheck extends TExtends ? TTrue : TFalse;
45+
46+
const fn9 = <Params>(
47+
params: Pick<Params, TemplatedConditional<keyof Params, never, never, keyof Params>>,
48+
): Params => params;
49+
50+
function fn10<T>(x: TemplatedConditional<T, never, never, T>) {
51+
var y: T = x;
52+
x = y;
53+
}
54+
55+
const fn11 = <Params>(
56+
params: Pick<Params, TemplatedConditional<keyof Params, keyof Params, keyof Params, never>>,
57+
): Params => params;
58+
59+
function fn12<T>(x: TemplatedConditional<T, T, T, never>) {
60+
var y: T = x;
61+
x = y;
62+
}
63+
64+
declare var z: any;
65+
const zee = z!!!; // since x is `any`, `x extends null | undefined` should be both true and false - and thus yield `any`
66+
67+
68+
//// [conditionalTypesSimplifyWhenTrivial.js]
69+
"use strict";
70+
var fn1 = function (params) { return params; };
71+
function fn2(x) {
72+
var y = x;
73+
x = y;
74+
}
75+
var fn3 = function (params) { return params; };
76+
function fn4(x) {
77+
var y = x;
78+
x = y;
79+
}
80+
var fn5 = function (params) { return params; };
81+
function fn6(x) {
82+
var y = x;
83+
x = y;
84+
}
85+
var fn7 = function (params) { return params; };
86+
function fn8(x) {
87+
var y = x;
88+
x = y;
89+
}
90+
var fn9 = function (params) { return params; };
91+
function fn10(x) {
92+
var y = x;
93+
x = y;
94+
}
95+
var fn11 = function (params) { return params; };
96+
function fn12(x) {
97+
var y = x;
98+
x = y;
99+
}
100+
var zee = z; // since x is `any`, `x extends null | undefined` should be both true and false - and thus yield `any`

0 commit comments

Comments
 (0)