Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Conditional type simplifications & Globally cached conditional type instances #29437

Merged
merged 18 commits into from
Mar 8, 2019
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 95 additions & 20 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 @@ -7459,15 +7460,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 @@ -7478,7 +7489,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 @@ -10072,12 +10089,47 @@ 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;
}
// Simplifications for types of the form `T extends U ? T : never` and `T extends U ? never : T`.
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)}`;
let result = conditionalTypes.get(instantiationId);
if (result) {
return result;
}
if (falseType.flags & TypeFlags.Never && isTypeIdenticalTo(getActualTypeVariable(trueType), getActualTypeVariable(checkType))) {
if (checkType.flags & TypeFlags.Any || isTypeAssignableTo(getRestrictiveInstantiation(checkType), getRestrictiveInstantiation(extendsType))) { // Always true
result = getDefaultConstraintOfTrueBranchOfConditionalType(root, /*combinedMapper*/ undefined, mapper);
}
else if (isIntersectionEmpty(checkType, extendsType)) { // Always false
result = neverType;
}
}
else if (trueType.flags & TypeFlags.Never && isTypeIdenticalTo(getActualTypeVariable(falseType), getActualTypeVariable(checkType))) {
if (!(checkType.flags & TypeFlags.Any) && isTypeAssignableTo(getRestrictiveInstantiation(checkType), getRestrictiveInstantiation(extendsType))) { // Always true
result = neverType;
}
else if (checkType.flags & TypeFlags.Any || isIntersectionEmpty(checkType, extendsType)) { // Always false
result = falseType; // TODO: Intersect negated `extends` type here
}
}
if (result) {
conditionalTypes.set(instantiationId, result);
return result;
}
const checkTypeInstantiable = maybeTypeOfKind(checkType, TypeFlags.Instantiable | TypeFlags.GenericMappedType);
let combinedMapper: TypeMapper | undefined;
if (root.inferTypeParameters) {
Expand All @@ -10095,38 +10147,49 @@ 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);
conditionalTypes.set(instantiationId, trueType);
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)]);
result = getUnionType([instantiateType(root.trueType, combinedMapper || mapper), falseType]);
conditionalTypes.set(instantiationId, result);
return result;
}
// 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);
conditionalTypes.set(instantiationId, falseType);
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
// that has no constraint. This ensures that, for example, the type
// type Foo<T extends { x: any }> = T extends { x: string } ? string : number
// doesn't immediately resolve to 'string' instead of being deferred.
if (isTypeAssignableTo(getRestrictiveInstantiation(checkType), getRestrictiveInstantiation(inferredExtendsType))) {
return instantiateType(root.trueType, combinedMapper || mapper);
result = instantiateType(root.trueType, combinedMapper || mapper);
conditionalTypes.set(instantiationId, result);
return result;
}
}
// Return a deferred type for a check that is neither definitely true nor definitely false
const erasedCheckType = getActualTypeVariable(checkType);
const result = <ConditionalType>createType(TypeFlags.Conditional);
result.root = root;
result.checkType = erasedCheckType;
result.extendsType = extendsType;
result.mapper = mapper;
result.combinedMapper = combinedMapper;
result = <ConditionalType>createType(TypeFlags.Conditional);
(result as ConditionalType).root = root;
(result as ConditionalType).checkType = erasedCheckType;
(result as ConditionalType).extendsType = extendsType;
(result as ConditionalType).mapper = mapper;
(result as ConditionalType).combinedMapper = combinedMapper;
if (!combinedMapper) {
(result as ConditionalType).resolvedTrueType = trueType;
(result as ConditionalType).resolvedFalseType = falseType;
}
result.aliasSymbol = root.aliasSymbol;
result.aliasTypeArguments = instantiateTypes(root.aliasTypeArguments, mapper!); // TODO: GH#18217
conditionalTypes.set(instantiationId, result);
return result;
}

Expand Down Expand Up @@ -11115,8 +11178,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