Skip to content

Commit

Permalink
Conditional type simplifications & Globally cached conditional type i…
Browse files Browse the repository at this point in the history
…nstances (#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
  • Loading branch information
weswigham authored Mar 8, 2019
1 parent 6607e00 commit a9ad94a
Show file tree
Hide file tree
Showing 12 changed files with 1,554 additions and 27 deletions.
97 changes: 84 additions & 13 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,7 @@ namespace ts {
const intersectionTypes = createMap<IntersectionType>();
const literalTypes = createMap<LiteralType>();
const indexedAccessTypes = createMap<IndexedAccessType>();
const conditionalTypes = createMap<Type>();
const evolvingArrayTypes: EvolvingArrayType[] = [];
const undefinedProperties = createMap<Symbol>() as UnderscoreEscapedMap<Symbol>;

Expand Down Expand Up @@ -7461,15 +7462,25 @@ namespace ts {
return baseConstraint && baseConstraint !== type ? baseConstraint : undefined;
}

function getDefaultConstraintOfTrueBranchOfConditionalType(root: ConditionalRoot, combinedMapper: TypeMapper | undefined, mapper: TypeMapper | undefined) {
const rootTrueType = root.trueType;
const rootTrueConstraint = !(rootTrueType.flags & TypeFlags.Substitution)
? rootTrueType
: instantiateType(((<SubstitutionType>rootTrueType).substitute), combinedMapper || mapper).flags & TypeFlags.AnyOrUnknown
? (<SubstitutionType>rootTrueType).typeVariable
: getIntersectionType([(<SubstitutionType>rootTrueType).substitute, (<SubstitutionType>rootTrueType).typeVariable]);
return instantiateType(rootTrueConstraint, combinedMapper || mapper);
}

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

/**
* Invokes union simplification logic to determine if an intersection is considered empty as a union constituent
*/
function isIntersectionEmpty(type1: Type, type2: Type) {
return !!(getUnionType([intersectTypes(type1, type2), neverType]).flags & TypeFlags.Never);
}

function getConditionalType(root: ConditionalRoot, mapper: TypeMapper | undefined): Type {
const checkType = instantiateType(root.checkType, mapper);
const extendsType = instantiateType(root.extendsType, mapper);
if (checkType === wildcardType || extendsType === wildcardType) {
return wildcardType;
}
const trueType = instantiateType(root.trueType, mapper);
const falseType = instantiateType(root.falseType, mapper);
const instantiationId = `${root.isDistributive ? "d" : ""}${getTypeId(checkType)}>${getTypeId(extendsType)}?${getTypeId(trueType)}:${getTypeId(falseType)}`;
const result = conditionalTypes.get(instantiationId);
if (result) {
return result;
}
const newResult = getConditionalTypeWorker(root, mapper, checkType, extendsType, trueType, falseType);
conditionalTypes.set(instantiationId, newResult);
return newResult;
}

function getConditionalTypeWorker(root: ConditionalRoot, mapper: TypeMapper | undefined, checkType: Type, extendsType: Type, trueType: Type, falseType: Type) {
// Simplifications for types of the form `T extends U ? T : never` and `T extends U ? never : T`.
if (falseType.flags & TypeFlags.Never && isTypeIdenticalTo(getActualTypeVariable(trueType), getActualTypeVariable(checkType))) {
if (checkType.flags & TypeFlags.Any || isTypeAssignableTo(getRestrictiveInstantiation(checkType), getRestrictiveInstantiation(extendsType))) { // Always true
return getDefaultConstraintOfTrueBranchOfConditionalType(root, /*combinedMapper*/ undefined, mapper);
}
else if (isIntersectionEmpty(checkType, extendsType)) { // Always false
return neverType;
}
}
else if (trueType.flags & TypeFlags.Never && isTypeIdenticalTo(getActualTypeVariable(falseType), getActualTypeVariable(checkType))) {
if (!(checkType.flags & TypeFlags.Any) && isTypeAssignableTo(getRestrictiveInstantiation(checkType), getRestrictiveInstantiation(extendsType))) { // Always true
return neverType;
}
else if (checkType.flags & TypeFlags.Any || isIntersectionEmpty(checkType, extendsType)) { // Always false
return falseType; // TODO: Intersect negated `extends` type here
}
}

const checkTypeInstantiable = maybeTypeOfKind(checkType, TypeFlags.Instantiable | TypeFlags.GenericMappedType);
let combinedMapper: TypeMapper | undefined;
if (root.inferTypeParameters) {
Expand All @@ -10112,18 +10167,18 @@ namespace ts {
// We attempt to resolve the conditional type only when the check and extends types are non-generic
if (!checkTypeInstantiable && !maybeTypeOfKind(inferredExtendsType, TypeFlags.Instantiable | TypeFlags.GenericMappedType)) {
if (inferredExtendsType.flags & TypeFlags.AnyOrUnknown) {
return instantiateType(root.trueType, mapper);
return trueType;
}
// Return union of trueType and falseType for 'any' since it matches anything
if (checkType.flags & TypeFlags.Any) {
return getUnionType([instantiateType(root.trueType, combinedMapper || mapper), instantiateType(root.falseType, mapper)]);
return getUnionType([instantiateType(root.trueType, combinedMapper || mapper), falseType]);
}
// Return falseType for a definitely false extends check. We check an instantiations of the two
// types with type parameters mapped to the wildcard type, the most permissive instantiations
// possible (the wildcard type is assignable to and from all types). If those are not related,
// then no instantiations will be and we can just return the false branch type.
if (!isTypeAssignableTo(getPermissiveInstantiation(checkType), getPermissiveInstantiation(inferredExtendsType))) {
return instantiateType(root.falseType, mapper);
return falseType;
}
// Return trueType for a definitely true extends check. We check instantiations of the two
// types with type parameters mapped to their restrictive form, i.e. a form of the type parameter
Expand All @@ -10142,6 +10197,10 @@ namespace ts {
result.extendsType = extendsType;
result.mapper = mapper;
result.combinedMapper = combinedMapper;
if (!combinedMapper) {
result.resolvedTrueType = trueType;
result.resolvedFalseType = falseType;
}
result.aliasSymbol = root.aliasSymbol;
result.aliasTypeArguments = instantiateTypes(root.aliasTypeArguments, mapper!); // TODO: GH#18217
return result;
Expand Down Expand Up @@ -11132,8 +11191,20 @@ namespace ts {
}

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

function instantiateIndexInfo(info: IndexInfo | undefined, mapper: TypeMapper): IndexInfo | undefined {
Expand Down
4 changes: 3 additions & 1 deletion tests/baselines/reference/conditionalTypes1.js
Original file line number Diff line number Diff line change
Expand Up @@ -509,6 +509,8 @@ declare function f5<T extends Options, K extends string>(p: K): Extract<T, {
}>;
declare let x0: {
k: "a";
} & {
k: "a";
a: number;
};
declare type OptionsOfKind<K extends Options["k"]> = Extract<Options, {
Expand Down Expand Up @@ -645,7 +647,7 @@ declare type T82 = Eq2<false, true>;
declare type T83 = Eq2<false, false>;
declare type Foo<T> = T extends string ? boolean : number;
declare type Bar<T> = T extends string ? boolean : number;
declare const convert: <U>(value: Foo<U>) => Bar<U>;
declare const convert: <U>(value: Foo<U>) => Foo<U>;
declare type Baz<T> = Foo<T>;
declare const convert2: <T>(value: Foo<T>) => Foo<T>;
declare function f31<T>(): void;
Expand Down
20 changes: 10 additions & 10 deletions tests/baselines/reference/conditionalTypes1.types
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ type T02 = Exclude<string | number | (() => void), Function>; // string | numbe
>T02 : string | number

type T03 = Extract<string | number | (() => void), Function>; // () => void
>T03 : () => void
>T03 : Function & (() => void)

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

type T11 = Extract<Options, { k: "a" | "b" }>; // { k: "a", a: number } | { k: "b", b: string }
>T11 : { k: "a"; a: number; } | { k: "b"; b: string; }
>T11 : ({ k: "a" | "b"; } & { k: "a"; a: number; }) | ({ k: "a" | "b"; } & { k: "b"; b: string; })
>k : "a" | "b"

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

type T13 = Extract<Options, { k: "a" } | { k: "b" }>; // { k: "a", a: number } | { k: "b", b: string }
>T13 : { k: "a"; a: number; } | { k: "b"; b: string; }
>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; })
>k : "a"
>k : "b"

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

let x0 = f5("a"); // { k: "a", a: number }
>x0 : { k: "a"; a: number; }
>f5("a") : { k: "a"; a: number; }
>x0 : { k: "a"; } & { k: "a"; a: number; }
>f5("a") : { k: "a"; } & { k: "a"; a: number; }
>f5 : <T extends Options, K extends string>(p: K) => Extract<T, { k: K; }>
>"a" : "a"

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

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

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

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

type TypeName<T> =
>TypeName : TypeName<T>
Expand Down Expand Up @@ -779,8 +779,8 @@ type Bar<T> = T extends string ? boolean : number;
>Bar : Bar<T>

const convert = <U>(value: Foo<U>): Bar<U> => value;
>convert : <U>(value: Foo<U>) => Bar<U>
><U>(value: Foo<U>): Bar<U> => value : <U>(value: Foo<U>) => Bar<U>
>convert : <U>(value: Foo<U>) => Foo<U>
><U>(value: Foo<U>): Bar<U> => value : <U>(value: Foo<U>) => Foo<U>
>value : Foo<U>
>value : Foo<U>

Expand Down Expand Up @@ -832,7 +832,7 @@ function f33<T, U>() {
>T1 : Foo<T & U>

type T2 = Bar<T & U>;
>T2 : Bar<T & U>
>T2 : Foo<T & U>

var z: T1;
>z : Foo<T & U>
Expand Down
6 changes: 3 additions & 3 deletions tests/baselines/reference/conditionalTypes2.types
Original file line number Diff line number Diff line change
Expand Up @@ -130,14 +130,14 @@ function f12(x: string | (() => string) | undefined) {
>x : string | (() => string) | undefined

const f = getFunction(x); // () => string
>f : () => string
>getFunction(x) : () => string
>f : Function & (() => string)
>getFunction(x) : Function & (() => string)
>getFunction : <T>(item: T) => Extract<T, Function>
>x : string | (() => string) | undefined

f();
>f() : string
>f : () => string
>f : Function & (() => string)
}

type Foo = { foo: string };
Expand Down
100 changes: 100 additions & 0 deletions tests/baselines/reference/conditionalTypesSimplifyWhenTrivial.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
//// [conditionalTypesSimplifyWhenTrivial.ts]
const fn1 = <Params>(
params: Pick<Params, Exclude<keyof Params, never>>,
): Params => params;

function fn2<T>(x: Exclude<T, never>) {
var y: T = x;
x = y;
}

const fn3 = <Params>(
params: Pick<Params, Extract<keyof Params, keyof Params>>,
): Params => params;

function fn4<T>(x: Extract<T, T>) {
var y: T = x;
x = y;
}

declare var x: Extract<number | string, any>; // Should be `numebr | string` and not `any`

type ExtractWithDefault<T, U, D = never> = T extends U ? T : D;

type ExcludeWithDefault<T, U, D = never> = T extends U ? D : T;

const fn5 = <Params>(
params: Pick<Params, ExcludeWithDefault<keyof Params, never>>,
): Params => params;

function fn6<T>(x: ExcludeWithDefault<T, never>) {
var y: T = x;
x = y;
}

const fn7 = <Params>(
params: Pick<Params, ExtractWithDefault<keyof Params, keyof Params>>,
): Params => params;

function fn8<T>(x: ExtractWithDefault<T, T>) {
var y: T = x;
x = y;
}

type TemplatedConditional<TCheck, TExtends, TTrue, TFalse> = TCheck extends TExtends ? TTrue : TFalse;

const fn9 = <Params>(
params: Pick<Params, TemplatedConditional<keyof Params, never, never, keyof Params>>,
): Params => params;

function fn10<T>(x: TemplatedConditional<T, never, never, T>) {
var y: T = x;
x = y;
}

const fn11 = <Params>(
params: Pick<Params, TemplatedConditional<keyof Params, keyof Params, keyof Params, never>>,
): Params => params;

function fn12<T>(x: TemplatedConditional<T, T, T, never>) {
var y: T = x;
x = y;
}

declare var z: any;
const zee = z!!!; // since x is `any`, `x extends null | undefined` should be both true and false - and thus yield `any`


//// [conditionalTypesSimplifyWhenTrivial.js]
"use strict";
var fn1 = function (params) { return params; };
function fn2(x) {
var y = x;
x = y;
}
var fn3 = function (params) { return params; };
function fn4(x) {
var y = x;
x = y;
}
var fn5 = function (params) { return params; };
function fn6(x) {
var y = x;
x = y;
}
var fn7 = function (params) { return params; };
function fn8(x) {
var y = x;
x = y;
}
var fn9 = function (params) { return params; };
function fn10(x) {
var y = x;
x = y;
}
var fn11 = function (params) { return params; };
function fn12(x) {
var y = x;
x = y;
}
var zee = z; // since x is `any`, `x extends null | undefined` should be both true and false - and thus yield `any`
Loading

0 comments on commit a9ad94a

Please sign in to comment.