Skip to content

Defer conditional types with multi-element tuple types in extends clause #52091

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

Merged
merged 3 commits into from
Jan 6, 2023
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
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
44 changes: 18 additions & 26 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17302,25 +17302,13 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
return constraint && (isGenericObjectType(constraint) || isGenericIndexType(constraint)) ? cloneTypeParameter(p) : p;
}

function isTypicalNondistributiveConditional(root: ConditionalRoot) {
return !root.isDistributive && isSingletonTupleType(root.node.checkType) && isSingletonTupleType(root.node.extendsType);
function isSimpleTupleType(node: TypeNode) {
return isTupleTypeNode(node) && length(node.elements) > 0 &&
!some(node.elements, e => isOptionalTypeNode(e) || isRestTypeNode(e) || isNamedTupleMember(e) && !!(e.questionToken || e.dotDotDotToken));
}

function isSingletonTupleType(node: TypeNode) {
return isTupleTypeNode(node) &&
length(node.elements) === 1 &&
!isOptionalTypeNode(node.elements[0]) &&
!isRestTypeNode(node.elements[0]) &&
!(isNamedTupleMember(node.elements[0]) && (node.elements[0].questionToken || node.elements[0].dotDotDotToken));
}

/**
* We syntactually check for common nondistributive conditional shapes and unwrap them into
* the intended comparison - we do this so we can check if the unwrapped types are generic or
* not and appropriately defer condition calculation
*/
function unwrapNondistributiveConditionalTuple(root: ConditionalRoot, type: Type) {
return isTypicalNondistributiveConditional(root) && isTupleType(type) ? getTypeArguments(type)[0] : type;
function isDeferredType(type: Type, checkTuples: boolean) {
return isGenericType(type) || checkTuples && isTupleType(type) && some(getTypeArguments(type), isGenericType);
}

function getConditionalType(root: ConditionalRoot, mapper: TypeMapper | undefined, aliasSymbol?: Symbol, aliasTypeArguments?: readonly Type[]): Type {
Expand All @@ -17338,10 +17326,14 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
result = errorType;
break;
}
const isUnwrapped = isTypicalNondistributiveConditional(root);
const checkType = instantiateType(unwrapNondistributiveConditionalTuple(root, getActualTypeVariable(root.checkType)), mapper);
const checkTypeInstantiable = isGenericType(checkType);
const extendsType = instantiateType(unwrapNondistributiveConditionalTuple(root, root.extendsType), mapper);
// When the check and extends types are simple tuple types of the same arity, we defer resolution of the
// conditional type when any tuple elements are generic. This is such that non-distributable conditional
// types can be written `[X] extends [Y] ? ...` and be deferred similarly to `X extends Y ? ...`.
const checkTuples = isSimpleTupleType(root.node.checkType) && isSimpleTupleType(root.node.extendsType) &&
length((root.node.checkType as TupleTypeNode).elements) === length((root.node.extendsType as TupleTypeNode).elements);
const checkType = instantiateType(getActualTypeVariable(root.checkType), mapper);
const checkTypeDeferred = isDeferredType(checkType, checkTuples);
const extendsType = instantiateType(root.extendsType, mapper);
if (checkType === wildcardType || extendsType === wildcardType) {
return wildcardType;
}
Expand Down Expand Up @@ -17375,7 +17367,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
}
}
}
if (!checkTypeInstantiable) {
if (!checkTypeDeferred) {
// We don't want inferences from constraints as they may cause us to eagerly resolve the
// conditional type instead of deferring resolution. Also, we always want strict function
// types rules (i.e. proper contravariance) for inferences.
Expand All @@ -17388,16 +17380,16 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
combinedMapper = mapper ? combineTypeMappers(innerMapper, mapper) : innerMapper;
}
// Instantiate the extends type including inferences for 'infer T' type parameters
const inferredExtendsType = combinedMapper ? instantiateType(unwrapNondistributiveConditionalTuple(root, root.extendsType), combinedMapper) : extendsType;
const inferredExtendsType = combinedMapper ? instantiateType(root.extendsType, combinedMapper) : extendsType;
// We attempt to resolve the conditional type only when the check and extends types are non-generic
if (!checkTypeInstantiable && !isGenericType(inferredExtendsType)) {
if (!checkTypeDeferred && !isDeferredType(inferredExtendsType, checkTuples)) {
// 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 (!(inferredExtendsType.flags & TypeFlags.AnyOrUnknown) && ((checkType.flags & TypeFlags.Any && !isUnwrapped) || !isTypeAssignableTo(getPermissiveInstantiation(checkType), getPermissiveInstantiation(inferredExtendsType)))) {
if (!(inferredExtendsType.flags & TypeFlags.AnyOrUnknown) && (checkType.flags & TypeFlags.Any || !isTypeAssignableTo(getPermissiveInstantiation(checkType), getPermissiveInstantiation(inferredExtendsType)))) {
// Return union of trueType and falseType for 'any' since it matches anything
if (checkType.flags & TypeFlags.Any && !isUnwrapped) {
if (checkType.flags & TypeFlags.Any) {
(extraTypes || (extraTypes = [])).push(instantiateType(getTypeFromTypeNode(root.node.trueType), combinedMapper || mapper));
}
// If falseType is an immediately nested conditional type that isn't distributive or has an
Expand Down
23 changes: 23 additions & 0 deletions tests/baselines/reference/deferredConditionalTypes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
//// [deferredConditionalTypes.ts]
type T0<X, Y> = X extends Y ? true : false; // Deferred
type T1<X, Y> = [X] extends [Y] ? true : false; // Deferred
type T2<X, Y> = [X, X] extends [Y, Y] ? true : false; // Deferred
type T3<X, Y> = [X, X, X] extends [Y, Y, Y] ? true : false; // Deferred

type T4<X, Y> = [X] extends [Y, Y] ? true : false; // false
type T5<X, Y> = [X, X] extends [Y] ? true : false; // false

// Repro from #52068

type Or<A extends boolean, B extends boolean> = [A, B] extends [false, false] ? false : true;
type And<A extends boolean, B extends boolean> = [A, B] extends [true, true] ? true : false;
type Not<T extends boolean> = T extends true ? false : true;
type Extends<A, B> = A extends B ? true : false;

type IsNumberLiteral<T> = And<Extends<T, number>, Not<Extends<number, T>>>;

type IsLiteral<T> = Or<false, IsNumberLiteral<T>>;


//// [deferredConditionalTypes.js]
"use strict";
96 changes: 96 additions & 0 deletions tests/baselines/reference/deferredConditionalTypes.symbols
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
=== tests/cases/compiler/deferredConditionalTypes.ts ===
type T0<X, Y> = X extends Y ? true : false; // Deferred
>T0 : Symbol(T0, Decl(deferredConditionalTypes.ts, 0, 0))
>X : Symbol(X, Decl(deferredConditionalTypes.ts, 0, 8))
>Y : Symbol(Y, Decl(deferredConditionalTypes.ts, 0, 10))
>X : Symbol(X, Decl(deferredConditionalTypes.ts, 0, 8))
>Y : Symbol(Y, Decl(deferredConditionalTypes.ts, 0, 10))

type T1<X, Y> = [X] extends [Y] ? true : false; // Deferred
>T1 : Symbol(T1, Decl(deferredConditionalTypes.ts, 0, 43))
>X : Symbol(X, Decl(deferredConditionalTypes.ts, 1, 8))
>Y : Symbol(Y, Decl(deferredConditionalTypes.ts, 1, 10))
>X : Symbol(X, Decl(deferredConditionalTypes.ts, 1, 8))
>Y : Symbol(Y, Decl(deferredConditionalTypes.ts, 1, 10))

type T2<X, Y> = [X, X] extends [Y, Y] ? true : false; // Deferred
>T2 : Symbol(T2, Decl(deferredConditionalTypes.ts, 1, 47))
>X : Symbol(X, Decl(deferredConditionalTypes.ts, 2, 8))
>Y : Symbol(Y, Decl(deferredConditionalTypes.ts, 2, 10))
>X : Symbol(X, Decl(deferredConditionalTypes.ts, 2, 8))
>X : Symbol(X, Decl(deferredConditionalTypes.ts, 2, 8))
>Y : Symbol(Y, Decl(deferredConditionalTypes.ts, 2, 10))
>Y : Symbol(Y, Decl(deferredConditionalTypes.ts, 2, 10))

type T3<X, Y> = [X, X, X] extends [Y, Y, Y] ? true : false; // Deferred
>T3 : Symbol(T3, Decl(deferredConditionalTypes.ts, 2, 53))
>X : Symbol(X, Decl(deferredConditionalTypes.ts, 3, 8))
>Y : Symbol(Y, Decl(deferredConditionalTypes.ts, 3, 10))
>X : Symbol(X, Decl(deferredConditionalTypes.ts, 3, 8))
>X : Symbol(X, Decl(deferredConditionalTypes.ts, 3, 8))
>X : Symbol(X, Decl(deferredConditionalTypes.ts, 3, 8))
>Y : Symbol(Y, Decl(deferredConditionalTypes.ts, 3, 10))
>Y : Symbol(Y, Decl(deferredConditionalTypes.ts, 3, 10))
>Y : Symbol(Y, Decl(deferredConditionalTypes.ts, 3, 10))

type T4<X, Y> = [X] extends [Y, Y] ? true : false; // false
>T4 : Symbol(T4, Decl(deferredConditionalTypes.ts, 3, 59))
>X : Symbol(X, Decl(deferredConditionalTypes.ts, 5, 8))
>Y : Symbol(Y, Decl(deferredConditionalTypes.ts, 5, 10))
>X : Symbol(X, Decl(deferredConditionalTypes.ts, 5, 8))
>Y : Symbol(Y, Decl(deferredConditionalTypes.ts, 5, 10))
>Y : Symbol(Y, Decl(deferredConditionalTypes.ts, 5, 10))

type T5<X, Y> = [X, X] extends [Y] ? true : false; // false
>T5 : Symbol(T5, Decl(deferredConditionalTypes.ts, 5, 50))
>X : Symbol(X, Decl(deferredConditionalTypes.ts, 6, 8))
>Y : Symbol(Y, Decl(deferredConditionalTypes.ts, 6, 10))
>X : Symbol(X, Decl(deferredConditionalTypes.ts, 6, 8))
>X : Symbol(X, Decl(deferredConditionalTypes.ts, 6, 8))
>Y : Symbol(Y, Decl(deferredConditionalTypes.ts, 6, 10))

// Repro from #52068

type Or<A extends boolean, B extends boolean> = [A, B] extends [false, false] ? false : true;
>Or : Symbol(Or, Decl(deferredConditionalTypes.ts, 6, 50))
>A : Symbol(A, Decl(deferredConditionalTypes.ts, 10, 8))
>B : Symbol(B, Decl(deferredConditionalTypes.ts, 10, 26))
>A : Symbol(A, Decl(deferredConditionalTypes.ts, 10, 8))
>B : Symbol(B, Decl(deferredConditionalTypes.ts, 10, 26))

type And<A extends boolean, B extends boolean> = [A, B] extends [true, true] ? true : false;
>And : Symbol(And, Decl(deferredConditionalTypes.ts, 10, 93))
>A : Symbol(A, Decl(deferredConditionalTypes.ts, 11, 9))
>B : Symbol(B, Decl(deferredConditionalTypes.ts, 11, 27))
>A : Symbol(A, Decl(deferredConditionalTypes.ts, 11, 9))
>B : Symbol(B, Decl(deferredConditionalTypes.ts, 11, 27))

type Not<T extends boolean> = T extends true ? false : true;
>Not : Symbol(Not, Decl(deferredConditionalTypes.ts, 11, 92))
>T : Symbol(T, Decl(deferredConditionalTypes.ts, 12, 9))
>T : Symbol(T, Decl(deferredConditionalTypes.ts, 12, 9))

type Extends<A, B> = A extends B ? true : false;
>Extends : Symbol(Extends, Decl(deferredConditionalTypes.ts, 12, 60))
>A : Symbol(A, Decl(deferredConditionalTypes.ts, 13, 13))
>B : Symbol(B, Decl(deferredConditionalTypes.ts, 13, 15))
>A : Symbol(A, Decl(deferredConditionalTypes.ts, 13, 13))
>B : Symbol(B, Decl(deferredConditionalTypes.ts, 13, 15))

type IsNumberLiteral<T> = And<Extends<T, number>, Not<Extends<number, T>>>;
>IsNumberLiteral : Symbol(IsNumberLiteral, Decl(deferredConditionalTypes.ts, 13, 48))
>T : Symbol(T, Decl(deferredConditionalTypes.ts, 15, 21))
>And : Symbol(And, Decl(deferredConditionalTypes.ts, 10, 93))
>Extends : Symbol(Extends, Decl(deferredConditionalTypes.ts, 12, 60))
>T : Symbol(T, Decl(deferredConditionalTypes.ts, 15, 21))
>Not : Symbol(Not, Decl(deferredConditionalTypes.ts, 11, 92))
>Extends : Symbol(Extends, Decl(deferredConditionalTypes.ts, 12, 60))
>T : Symbol(T, Decl(deferredConditionalTypes.ts, 15, 21))

type IsLiteral<T> = Or<false, IsNumberLiteral<T>>;
>IsLiteral : Symbol(IsLiteral, Decl(deferredConditionalTypes.ts, 15, 75))
>T : Symbol(T, Decl(deferredConditionalTypes.ts, 17, 15))
>Or : Symbol(Or, Decl(deferredConditionalTypes.ts, 6, 50))
>IsNumberLiteral : Symbol(IsNumberLiteral, Decl(deferredConditionalTypes.ts, 13, 48))
>T : Symbol(T, Decl(deferredConditionalTypes.ts, 17, 15))

65 changes: 65 additions & 0 deletions tests/baselines/reference/deferredConditionalTypes.types
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
=== tests/cases/compiler/deferredConditionalTypes.ts ===
type T0<X, Y> = X extends Y ? true : false; // Deferred
>T0 : T0<X, Y>
>true : true
>false : false

type T1<X, Y> = [X] extends [Y] ? true : false; // Deferred
>T1 : T1<X, Y>
>true : true
>false : false

type T2<X, Y> = [X, X] extends [Y, Y] ? true : false; // Deferred
>T2 : T2<X, Y>
>true : true
>false : false

type T3<X, Y> = [X, X, X] extends [Y, Y, Y] ? true : false; // Deferred
>T3 : T3<X, Y>
>true : true
>false : false

type T4<X, Y> = [X] extends [Y, Y] ? true : false; // false
>T4 : false
>true : true
>false : false

type T5<X, Y> = [X, X] extends [Y] ? true : false; // false
>T5 : false
>true : true
>false : false

// Repro from #52068

type Or<A extends boolean, B extends boolean> = [A, B] extends [false, false] ? false : true;
>Or : Or<A, B>
>false : false
>false : false
>false : false
>true : true

type And<A extends boolean, B extends boolean> = [A, B] extends [true, true] ? true : false;
>And : And<A, B>
>true : true
>true : true
>true : true
>false : false

type Not<T extends boolean> = T extends true ? false : true;
>Not : Not<T>
>true : true
>false : false
>true : true

type Extends<A, B> = A extends B ? true : false;
>Extends : Extends<A, B>
>true : true
>false : false

type IsNumberLiteral<T> = And<Extends<T, number>, Not<Extends<number, T>>>;
>IsNumberLiteral : IsNumberLiteral<T>

type IsLiteral<T> = Or<false, IsNumberLiteral<T>>;
>IsLiteral : IsLiteral<T>
>false : false

20 changes: 20 additions & 0 deletions tests/cases/compiler/deferredConditionalTypes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// @strict: true

type T0<X, Y> = X extends Y ? true : false; // Deferred
type T1<X, Y> = [X] extends [Y] ? true : false; // Deferred
type T2<X, Y> = [X, X] extends [Y, Y] ? true : false; // Deferred
type T3<X, Y> = [X, X, X] extends [Y, Y, Y] ? true : false; // Deferred

type T4<X, Y> = [X] extends [Y, Y] ? true : false; // false
type T5<X, Y> = [X, X] extends [Y] ? true : false; // false

// Repro from #52068

type Or<A extends boolean, B extends boolean> = [A, B] extends [false, false] ? false : true;
type And<A extends boolean, B extends boolean> = [A, B] extends [true, true] ? true : false;
type Not<T extends boolean> = T extends true ? false : true;
type Extends<A, B> = A extends B ? true : false;

type IsNumberLiteral<T> = And<Extends<T, number>, Not<Extends<number, T>>>;

type IsLiteral<T> = Or<false, IsNumberLiteral<T>>;