Skip to content

Add assignability rules for when the target is a conditional type #27932

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

Closed
Closed
Show file tree
Hide file tree
Changes from all 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
120 changes: 120 additions & 0 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -662,6 +662,7 @@ namespace ts {
ImmediateBaseConstraint,
EnumTagType,
JSDocTypeReference,
NeverInstantiable,
}

const enum CheckMode {
Expand Down Expand Up @@ -4520,6 +4521,8 @@ namespace ts {
return !!(<Type>target).immediateBaseConstraint;
case TypeSystemPropertyName.JSDocTypeReference:
return !!getSymbolLinks(target as Symbol).resolvedJSDocType;
case TypeSystemPropertyName.NeverInstantiable:
return typeof (target as Type).instantiableToNever !== "undefined";
}
return Debug.assertNever(propertyName);
}
Expand Down Expand Up @@ -11126,6 +11129,12 @@ namespace ts {
!t.numberIndexInfo;
}

function isKeylessResolvedType(t: ResolvedType) {
return t.properties.length === 0 &&
!t.stringIndexInfo &&
!t.numberIndexInfo;
}

function isEmptyObjectType(type: Type): boolean {
return type.flags & TypeFlags.Object ? isEmptyResolvedType(resolveStructuredTypeMembers(<ObjectType>type)) :
type.flags & TypeFlags.NonPrimitive ? true :
Expand All @@ -11134,6 +11143,14 @@ namespace ts {
false;
}

function isKeylessObjectType(type: Type): boolean {
return type.flags & TypeFlags.Object ? isKeylessResolvedType(resolveStructuredTypeMembers(<ObjectType>type)) :
type.flags & TypeFlags.NonPrimitive ? true :
type.flags & TypeFlags.Union ? some((<UnionType>type).types, isKeylessObjectType) :
type.flags & TypeFlags.Intersection ? every((<UnionType>type).types, isKeylessObjectType) :
false;
}

function isEnumTypeRelatedTo(sourceSymbol: Symbol, targetSymbol: Symbol, errorReporter?: ErrorReporter) {
if (sourceSymbol === targetSymbol) {
return true;
Expand Down Expand Up @@ -11854,6 +11871,70 @@ namespace ts {
return relation === definitelyAssignableRelation ? undefined : getConstraintOfType(type);
}

function canBeKeyless(type: Type): boolean {
if (canBeNever(type)) {
return true; // `keyof never` is `never`, ergo `never` counts as keyless even though conceptually it contains every key
}
if (type.flags & TypeFlags.Intersection) {
return every((type as IntersectionType).types, canBeKeyless);
}
if (type.flags & TypeFlags.Union) {
return some((type as UnionType).types, canBeKeyless);
}
if (type.flags & TypeFlags.Conditional) {
return canBeKeyless(getTrueTypeFromConditionalType(type as ConditionalType)) || canBeKeyless(getFalseTypeFromConditionalType(type as ConditionalType));
}
if (type.flags & TypeFlags.Index) {
return canBeKeyless((type as IndexType).type);
}
if (type.flags & TypeFlags.IndexedAccess) {
return canBeKeyless((type as IndexedAccessType).objectType);
}
if (type.flags & TypeFlags.TypeParameter) {
const constraint = getConstraintOfTypeParameter(type as TypeParameter);
return !constraint || canBeKeyless(constraint);
}
if (type.flags & TypeFlags.Substitution) {
return canBeKeyless((type as SubstitutionType).substitute) && canBeKeyless((type as SubstitutionType).typeVariable);
}
return !!(type.flags & TypeFlags.Unknown) || isKeylessObjectType(type);
}

function canBeNever(type: Type): boolean {
if (!pushTypeResolution(type, TypeSystemPropertyName.NeverInstantiable)) {
return type.instantiableToNever = true;
}
let result = true;
if (type.flags & TypeFlags.Intersection) {
result = some((type as IntersectionType).types, canBeNever);
}
else if (type.flags & TypeFlags.Union) {
result = every((type as UnionType).types, canBeNever);
}
else if (type.flags & TypeFlags.Conditional) {
result = ((type as ConditionalType).root.isDistributive && canBeNever((type as ConditionalType).checkType)) ||
canBeNever(getTrueTypeFromConditionalType(type as ConditionalType)) ||
canBeNever(getFalseTypeFromConditionalType(type as ConditionalType));
}
else if (type.flags & TypeFlags.Index) {
result = canBeKeyless((type as IndexType).type);
}
else if (type.flags & TypeFlags.IndexedAccess) {
result = canBeNever((type as IndexedAccessType).indexType) || canBeNever((type as IndexedAccessType).objectType);
}
else if (type.flags & (TypeFlags.TypeParameter | TypeFlags.Substitution)) {
// If we ever get `super` constraints, adjust this
result = true;
}
else {
result = !!(type.flags & TypeFlags.Never);
}
if (!popTypeResolution()) {
return type.instantiableToNever = true;
}
return type.instantiableToNever = result;
}

function structuredTypeRelatedTo(source: Type, target: Type, reportErrors: boolean, isIntersectionConstituent: boolean): Ternary {
const flags = source.flags & target.flags;
if (relation === identityRelation && !(flags & TypeFlags.Object)) {
Expand Down Expand Up @@ -11955,6 +12036,45 @@ namespace ts {
}
}
}
else if (target.flags & TypeFlags.Conditional) {
const root = (target as ConditionalType).root;
if (!root.isDistributive || !canBeNever((target as ConditionalType).checkType)) {
if (root.inferTypeParameters) {
// If the constraint indicates that the conditional type is always true (but it is stil deferred to allow for, eg, distribution or inference)
// We should perform the instantiation and only check against the true type
const mapper = (target as ConditionalType).mapper;
const context = createInferenceContext(root.inferTypeParameters, /*signature*/ undefined, InferenceFlags.None);
const instantiatedExtends = instantiateType(root.extendsType, mapper);
const checkConstraint = getSimplifiedType(instantiateType(root.checkType, mapper));
// TODO:
// As-is, this is effectively sound, but not particularly useful, thanks to all the types it wrongly rejects - only
// conditional types with effectively "independent" inference parameters will end up being assignable via this branch, eg
// `type InferBecauseWhyNot<T> = T extends (p: infer P1) => any ? T | P1 : never;`
// contains a union in the `true` branch, and so while we can't confirm assignability to `P1`, we can confirm assignability to `T`.
// A lenient version could be made by replacing `getintersectionType([instantiateType(root.trueType, combinedMapper), instantiateType(root.trueType, mapper)])`
// with `instantiateType(root.trueType, combinedMapper)` which would skip checking aginst the type-parametery-ness of the check;
// but such a change introduces quite a bit of unsoundness as we stop checking against the type-parameteryness of the `infer` type,
// which in turn prevents us from erroring on, eg, unsafe write-position assignments of the constraint of the type.
// To be correct here, we'd need to track the implied variance of the infer parameters and _infer_ appropriately (in addition to checking appropriately)
// Specifically, we'd need to infer with `InferencePriority.NoConstraint` (or ideally a hypothetical `InferencePriority.SuperConstraint`) for contravariant types,
// but continue using the constraints for covariant ones.
inferTypes(context.inferences, checkConstraint, instantiatedExtends, InferencePriority.AlwaysStrict);
const combinedMapper = combineTypeMappers(mapper, context);
if (isRelatedTo(checkConstraint, instantiateType(root.extendsType, combinedMapper))) {
if (result = isRelatedTo(source, getIntersectionType([instantiateType(root.trueType, combinedMapper), instantiateType(root.trueType, mapper)]), reportErrors)) {
errorInfo = saveErrorInfo;
return result;
}
}
}
if (result = isRelatedTo(source, getTrueTypeFromConditionalType(target as ConditionalType))) {
if (result &= isRelatedTo(source, getFalseTypeFromConditionalType(target as ConditionalType))) {
errorInfo = saveErrorInfo;
return result;
}
}
}
}

if (source.flags & TypeFlags.TypeVariable) {
if (source.flags & TypeFlags.IndexedAccess && target.flags & TypeFlags.IndexedAccess) {
Expand Down
2 changes: 2 additions & 0 deletions src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3894,6 +3894,8 @@ namespace ts {
wildcardInstantiation?: Type; // Instantiation with type parameters mapped to wildcard type
/* @internal */
immediateBaseConstraint?: Type; // Immediate base constraint cache
/* @internal */
instantiableToNever?: boolean; // Flag set by `canBeNever`
}

/* @internal */
Expand Down
38 changes: 37 additions & 1 deletion tests/baselines/reference/conditionalTypes2.errors.txt
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,13 @@ tests/cases/conformance/types/conditional/conditionalTypes2.ts(74,12): error TS2
tests/cases/conformance/types/conditional/conditionalTypes2.ts(75,12): error TS2345: Argument of type 'Extract2<T, Foo, Bar>' is not assignable to parameter of type '{ foo: string; bat: string; }'.
Type 'T extends Bar ? T : never' is not assignable to type '{ foo: string; bat: string; }'.
Type 'Bar & Foo & T' is not assignable to type '{ foo: string; bat: string; }'.
tests/cases/conformance/types/conditional/conditionalTypes2.ts(161,11): error TS2322: Type '{ a: number; b: number; }' is not assignable to type '[T] extends [[infer U]] ? U : { b: number; }'.
tests/cases/conformance/types/conditional/conditionalTypes2.ts(163,11): error TS2322: Type '{ a: number; b: number; }' is not assignable to type 'Distributive<T>'.
tests/cases/conformance/types/conditional/conditionalTypes2.ts(165,11): error TS2322: Type '{ a: number; b: number; }' is not assignable to type 'Distributive<T & string>'.
tests/cases/conformance/types/conditional/conditionalTypes2.ts(169,11): error TS2322: Type '{ a: number; b: number; }' is not assignable to type 'Distributive<[T] extends [never] ? { a: number; } : never>'.


==== tests/cases/conformance/types/conditional/conditionalTypes2.ts (7 errors) ====
==== tests/cases/conformance/types/conditional/conditionalTypes2.ts (11 errors) ====
interface Covariant<T> {
foo: T extends string ? T : number;
}
Expand Down Expand Up @@ -206,4 +210,36 @@ tests/cases/conformance/types/conditional/conditionalTypes2.ts(75,12): error TS2

type C2<T, V, E> =
T extends object ? { [Q in keyof T]: C2<T[Q], V, E>; } : T;

// #26933
type Distributive<T> = T extends { a: number } ? { a: number } : { b: number };
function testAssignabilityToConditionalType<T>() {
const o = { a: 1, b: 2 };
const x: [T] extends [string] ? { y: number } : { a: number, b: number } = undefined!;
// Simple case: OK
const o1: [T] extends [number] ? { a: number } : { b: number } = o;
// Simple case where source happens to be a conditional type: also OK
const x1: [T] extends [number]
? ([T] extends [string] ? { y: number } : { a: number })
: ([T] extends [string] ? { y: number } : { b: number })
= x;
// Infer type parameters: no good
const o2: [T] extends [[infer U]] ? U : { b: number } = o;
~~
!!! error TS2322: Type '{ a: number; b: number; }' is not assignable to type '[T] extends [[infer U]] ? U : { b: number; }'.
// Distributive where T might instantiate to never: no good
const o3: Distributive<T> = o;
~~
!!! error TS2322: Type '{ a: number; b: number; }' is not assignable to type 'Distributive<T>'.
// Distributive where T & string might instantiate to never: also no good
const o4: Distributive<T & string> = o;
~~
!!! error TS2322: Type '{ a: number; b: number; }' is not assignable to type 'Distributive<T & string>'.
// Distributive where {a: T} cannot instantiate to never: OK
const o5: Distributive<{ a: T }> = o;
// Distributive where check type is a conditional which returns a non-never type upon instantiation with `never` but can still return never otherwise: no good
const o6: Distributive<[T] extends [never] ? { a: number } : never> = o;
~~
!!! error TS2322: Type '{ a: number; b: number; }' is not assignable to type 'Distributive<[T] extends [never] ? { a: number; } : never>'.
}

50 changes: 50 additions & 0 deletions tests/baselines/reference/conditionalTypes2.js
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,30 @@ type B2<T, V> =

type C2<T, V, E> =
T extends object ? { [Q in keyof T]: C2<T[Q], V, E>; } : T;

// #26933
type Distributive<T> = T extends { a: number } ? { a: number } : { b: number };
function testAssignabilityToConditionalType<T>() {
const o = { a: 1, b: 2 };
const x: [T] extends [string] ? { y: number } : { a: number, b: number } = undefined!;
// Simple case: OK
const o1: [T] extends [number] ? { a: number } : { b: number } = o;
// Simple case where source happens to be a conditional type: also OK
const x1: [T] extends [number]
? ([T] extends [string] ? { y: number } : { a: number })
: ([T] extends [string] ? { y: number } : { b: number })
= x;
// Infer type parameters: no good
const o2: [T] extends [[infer U]] ? U : { b: number } = o;
// Distributive where T might instantiate to never: no good
const o3: Distributive<T> = o;
// Distributive where T & string might instantiate to never: also no good
const o4: Distributive<T & string> = o;
// Distributive where {a: T} cannot instantiate to never: OK
const o5: Distributive<{ a: T }> = o;
// Distributive where check type is a conditional which returns a non-never type upon instantiation with `never` but can still return never otherwise: no good
const o6: Distributive<[T] extends [never] ? { a: number } : never> = o;
}


//// [conditionalTypes2.js]
Expand Down Expand Up @@ -222,6 +246,24 @@ function foo(value) {
toString2(value);
}
}
function testAssignabilityToConditionalType() {
var o = { a: 1, b: 2 };
var x = undefined;
// Simple case: OK
var o1 = o;
// Simple case where source happens to be a conditional type: also OK
var x1 = x;
// Infer type parameters: no good
var o2 = o;
// Distributive where T might instantiate to never: no good
var o3 = o;
// Distributive where T & string might instantiate to never: also no good
var o4 = o;
// Distributive where {a: T} cannot instantiate to never: OK
var o5 = o;
// Distributive where check type is a conditional which returns a non-never type upon instantiation with `never` but can still return never otherwise: no good
var o6 = o;
}


//// [conditionalTypes2.d.ts]
Expand Down Expand Up @@ -304,3 +346,11 @@ declare type B2<T, V> = T extends object ? T extends any[] ? T : {
declare type C2<T, V, E> = T extends object ? {
[Q in keyof T]: C2<T[Q], V, E>;
} : T;
declare type Distributive<T> = T extends {
a: number;
} ? {
a: number;
} : {
b: number;
};
declare function testAssignabilityToConditionalType<T>(): void;
Loading