Skip to content

Commit 7652906

Browse files
committed
Stronger guarantees on edge case handling, stolen tests from #27589
1 parent abde557 commit 7652906

12 files changed

+465
-44
lines changed

src/compiler/checker.ts

+112-29
Original file line numberDiff line numberDiff line change
@@ -667,6 +667,7 @@ namespace ts {
667667
ImmediateBaseConstraint,
668668
EnumTagType,
669669
JSDocTypeReference,
670+
NeverInstantiable,
670671
}
671672

672673
const enum CheckMode {
@@ -4525,6 +4526,8 @@ namespace ts {
45254526
return !!(<Type>target).immediateBaseConstraint;
45264527
case TypeSystemPropertyName.JSDocTypeReference:
45274528
return !!getSymbolLinks(target as Symbol).resolvedJSDocType;
4529+
case TypeSystemPropertyName.NeverInstantiable:
4530+
return typeof (target as Type).instantiableToNever !== "undefined";
45284531
}
45294532
return Debug.assertNever(propertyName);
45304533
}
@@ -11130,6 +11133,12 @@ namespace ts {
1113011133
!t.numberIndexInfo;
1113111134
}
1113211135

11136+
function isKeylessResolvedType(t: ResolvedType) {
11137+
return t.properties.length === 0 &&
11138+
!t.stringIndexInfo &&
11139+
!t.numberIndexInfo;
11140+
}
11141+
1113311142
function isEmptyObjectType(type: Type): boolean {
1113411143
return type.flags & TypeFlags.Object ? isEmptyResolvedType(resolveStructuredTypeMembers(<ObjectType>type)) :
1113511144
type.flags & TypeFlags.NonPrimitive ? true :
@@ -11138,6 +11147,14 @@ namespace ts {
1113811147
false;
1113911148
}
1114011149

11150+
function isKeylessObjectType(type: Type): boolean {
11151+
return type.flags & TypeFlags.Object ? isKeylessResolvedType(resolveStructuredTypeMembers(<ObjectType>type)) :
11152+
type.flags & TypeFlags.NonPrimitive ? true :
11153+
type.flags & TypeFlags.Union ? some((<UnionType>type).types, isKeylessObjectType) :
11154+
type.flags & TypeFlags.Intersection ? every((<UnionType>type).types, isKeylessObjectType) :
11155+
false;
11156+
}
11157+
1114111158
function isEnumTypeRelatedTo(sourceSymbol: Symbol, targetSymbol: Symbol, errorReporter?: ErrorReporter) {
1114211159
if (sourceSymbol === targetSymbol) {
1114311160
return true;
@@ -11860,6 +11877,70 @@ namespace ts {
1186011877
return relation === definitelyAssignableRelation ? undefined : getConstraintOfType(type);
1186111878
}
1186211879

11880+
function canBeKeyless(type: Type): boolean {
11881+
if (canBeNever(type)) {
11882+
return true; // `keyof never` is `never`, ergo `never` counts as keyless even though conceptually it contains every key
11883+
}
11884+
if (type.flags & TypeFlags.Intersection) {
11885+
return every((type as IntersectionType).types, canBeKeyless);
11886+
}
11887+
if (type.flags & TypeFlags.Union) {
11888+
return some((type as UnionType).types, canBeKeyless);
11889+
}
11890+
if (type.flags & TypeFlags.Conditional) {
11891+
return canBeKeyless(getTrueTypeFromConditionalType(type as ConditionalType)) || canBeKeyless(getFalseTypeFromConditionalType(type as ConditionalType));
11892+
}
11893+
if (type.flags & TypeFlags.Index) {
11894+
return canBeKeyless((type as IndexType).type);
11895+
}
11896+
if (type.flags & TypeFlags.IndexedAccess) {
11897+
return canBeKeyless((type as IndexedAccessType).objectType);
11898+
}
11899+
if (type.flags & TypeFlags.TypeParameter) {
11900+
const constraint = getConstraintOfTypeParameter(type as TypeParameter);
11901+
return !constraint || canBeKeyless(constraint);
11902+
}
11903+
if (type.flags & TypeFlags.Substitution) {
11904+
return canBeKeyless((type as SubstitutionType).substitute) && canBeKeyless((type as SubstitutionType).typeVariable);
11905+
}
11906+
return !!(type.flags & TypeFlags.Unknown) || isKeylessObjectType(type);
11907+
}
11908+
11909+
function canBeNever(type: Type): boolean {
11910+
if (!pushTypeResolution(type, TypeSystemPropertyName.NeverInstantiable)) {
11911+
return type.instantiableToNever = true;
11912+
}
11913+
let result = true;
11914+
if (type.flags & TypeFlags.Intersection) {
11915+
result = some((type as IntersectionType).types, canBeNever);
11916+
}
11917+
else if (type.flags & TypeFlags.Union) {
11918+
result = every((type as UnionType).types, canBeNever);
11919+
}
11920+
else if (type.flags & TypeFlags.Conditional) {
11921+
result = ((type as ConditionalType).root.isDistributive && canBeNever((type as ConditionalType).checkType)) ||
11922+
canBeNever(getTrueTypeFromConditionalType(type as ConditionalType)) ||
11923+
canBeNever(getFalseTypeFromConditionalType(type as ConditionalType));
11924+
}
11925+
else if (type.flags & TypeFlags.Index) {
11926+
result = canBeKeyless((type as IndexType).type);
11927+
}
11928+
else if (type.flags & TypeFlags.IndexedAccess) {
11929+
result = canBeNever((type as IndexedAccessType).indexType) || canBeNever((type as IndexedAccessType).objectType);
11930+
}
11931+
else if (type.flags & (TypeFlags.TypeParameter | TypeFlags.Substitution)) {
11932+
// If we ever get `super` constraints, adjust this
11933+
result = true;
11934+
}
11935+
else {
11936+
result = !!(type.flags & TypeFlags.Never);
11937+
}
11938+
if (!popTypeResolution()) {
11939+
return type.instantiableToNever = true;
11940+
}
11941+
return type.instantiableToNever = result;
11942+
}
11943+
1186311944
function structuredTypeRelatedTo(source: Type, target: Type, reportErrors: boolean): Ternary {
1186411945
const flags = source.flags & target.flags;
1186511946
if (relation === identityRelation && !(flags & TypeFlags.Object)) {
@@ -11963,40 +12044,42 @@ namespace ts {
1196312044
}
1196412045
else if (target.flags & TypeFlags.Conditional) {
1196512046
const root = (target as ConditionalType).root;
11966-
if (root.inferTypeParameters) {
11967-
// If the constraint indicates that the conditional type is always true (but it is stil deferred to allow for, eg, distribution or inference)
11968-
// We should perform the instantiation and only check against the true type
11969-
const mapper = (target as ConditionalType).mapper;
11970-
const context = createInferenceContext(root.inferTypeParameters, /*signature*/ undefined, InferenceFlags.None);
11971-
const instantiatedExtends = instantiateType(root.extendsType, mapper);
11972-
const checkConstraint = getSimplifiedType(instantiateType(root.checkType, mapper));
11973-
// TODO:
11974-
// As-is, this is effectively sound, but not particularly useful, thanks to all the types it wrongly rejects - only
11975-
// conditional types with effectively "independent" inference parameters will end up being assignable via this branch, eg
11976-
// `type InferBecauseWhyNot<T> = T extends (p: infer P1) => any ? T | P1 : never;`
11977-
// contains a union in the `true` branch, and so while we can't confirm assignability to `P1`, we can confirm assignability to `T`.
11978-
// A lenient version could be made by replacing `getintersectionType([instantiateType(root.trueType, combinedMapper), instantiateType(root.trueType, mapper)])`
11979-
// with `instantiateType(root.trueType, combinedMapper)` which would skip checking aginst the type-parametery-ness of the check;
11980-
// but such a change introduces quite a bit of unsoundness as we stop checking against the type-parameteryness of the `infer` type,
11981-
// which in turn prevents us from erroring on, eg, unsafe write-position assignments of the constraint of the type.
11982-
// To be correct here, we'd need to track the implied variance of the infer parameters and _infer_ appropriately (in addition to checking appropriately)
11983-
// Specifically, we'd need to infer with `InferencePriority.NoConstraint` (or ideally a hypothetical `InferencePriority.SuperConstraint`) for contravariant types,
11984-
// but continue using the constraints for covariant ones.
11985-
inferTypes(context.inferences, checkConstraint, instantiatedExtends, InferencePriority.AlwaysStrict);
11986-
const combinedMapper = combineTypeMappers(mapper, context);
11987-
if (isRelatedTo(checkConstraint, instantiateType(root.extendsType, combinedMapper))) {
11988-
if (result = isRelatedTo(source, getIntersectionType([instantiateType(root.trueType, combinedMapper), instantiateType(root.trueType, mapper)]), reportErrors)) {
12047+
if (!root.isDistributive || !canBeNever((target as ConditionalType).checkType)) {
12048+
if (root.inferTypeParameters) {
12049+
// If the constraint indicates that the conditional type is always true (but it is stil deferred to allow for, eg, distribution or inference)
12050+
// We should perform the instantiation and only check against the true type
12051+
const mapper = (target as ConditionalType).mapper;
12052+
const context = createInferenceContext(root.inferTypeParameters, /*signature*/ undefined, InferenceFlags.None);
12053+
const instantiatedExtends = instantiateType(root.extendsType, mapper);
12054+
const checkConstraint = getSimplifiedType(instantiateType(root.checkType, mapper));
12055+
// TODO:
12056+
// As-is, this is effectively sound, but not particularly useful, thanks to all the types it wrongly rejects - only
12057+
// conditional types with effectively "independent" inference parameters will end up being assignable via this branch, eg
12058+
// `type InferBecauseWhyNot<T> = T extends (p: infer P1) => any ? T | P1 : never;`
12059+
// contains a union in the `true` branch, and so while we can't confirm assignability to `P1`, we can confirm assignability to `T`.
12060+
// A lenient version could be made by replacing `getintersectionType([instantiateType(root.trueType, combinedMapper), instantiateType(root.trueType, mapper)])`
12061+
// with `instantiateType(root.trueType, combinedMapper)` which would skip checking aginst the type-parametery-ness of the check;
12062+
// but such a change introduces quite a bit of unsoundness as we stop checking against the type-parameteryness of the `infer` type,
12063+
// which in turn prevents us from erroring on, eg, unsafe write-position assignments of the constraint of the type.
12064+
// To be correct here, we'd need to track the implied variance of the infer parameters and _infer_ appropriately (in addition to checking appropriately)
12065+
// Specifically, we'd need to infer with `InferencePriority.NoConstraint` (or ideally a hypothetical `InferencePriority.SuperConstraint`) for contravariant types,
12066+
// but continue using the constraints for covariant ones.
12067+
inferTypes(context.inferences, checkConstraint, instantiatedExtends, InferencePriority.AlwaysStrict);
12068+
const combinedMapper = combineTypeMappers(mapper, context);
12069+
if (isRelatedTo(checkConstraint, instantiateType(root.extendsType, combinedMapper))) {
12070+
if (result = isRelatedTo(source, getIntersectionType([instantiateType(root.trueType, combinedMapper), instantiateType(root.trueType, mapper)]), reportErrors)) {
12071+
errorInfo = saveErrorInfo;
12072+
return result;
12073+
}
12074+
}
12075+
}
12076+
if (result = isRelatedTo(source, getTrueTypeFromConditionalType(target as ConditionalType))) {
12077+
if (result &= isRelatedTo(source, getFalseTypeFromConditionalType(target as ConditionalType))) {
1198912078
errorInfo = saveErrorInfo;
1199012079
return result;
1199112080
}
1199212081
}
1199312082
}
11994-
if (result = isRelatedTo(source, getTrueTypeFromConditionalType(target as ConditionalType))) {
11995-
if (result = isRelatedTo(source, getFalseTypeFromConditionalType(target as ConditionalType))) {
11996-
errorInfo = saveErrorInfo;
11997-
return result;
11998-
}
11999-
}
1200012083
}
1200112084

1200212085
if (source.flags & TypeFlags.TypeVariable) {

src/compiler/types.ts

+2
Original file line numberDiff line numberDiff line change
@@ -3897,6 +3897,8 @@ namespace ts {
38973897
wildcardInstantiation?: Type; // Instantiation with type parameters mapped to wildcard type
38983898
/* @internal */
38993899
immediateBaseConstraint?: Type; // Immediate base constraint cache
3900+
/* @internal */
3901+
instantiableToNever?: boolean; // Flag set by `canBeNever`
39003902
}
39013903

39023904
/* @internal */

tests/baselines/reference/conditionalTypes2.errors.txt

+37-1
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,13 @@ tests/cases/conformance/types/conditional/conditionalTypes2.ts(74,12): error TS2
2424
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; }'.
2525
Type 'T extends Bar ? T : never' is not assignable to type '{ foo: string; bat: string; }'.
2626
Type 'Bar & Foo & T' is not assignable to type '{ foo: string; bat: string; }'.
27+
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; }'.
28+
tests/cases/conformance/types/conditional/conditionalTypes2.ts(163,11): error TS2322: Type '{ a: number; b: number; }' is not assignable to type 'Distributive<T>'.
29+
tests/cases/conformance/types/conditional/conditionalTypes2.ts(165,11): error TS2322: Type '{ a: number; b: number; }' is not assignable to type 'Distributive<T & string>'.
30+
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>'.
2731

2832

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

207211
type C2<T, V, E> =
208212
T extends object ? { [Q in keyof T]: C2<T[Q], V, E>; } : T;
213+
214+
// #26933
215+
type Distributive<T> = T extends { a: number } ? { a: number } : { b: number };
216+
function testAssignabilityToConditionalType<T>() {
217+
const o = { a: 1, b: 2 };
218+
const x: [T] extends [string] ? { y: number } : { a: number, b: number } = undefined!;
219+
// Simple case: OK
220+
const o1: [T] extends [number] ? { a: number } : { b: number } = o;
221+
// Simple case where source happens to be a conditional type: also OK
222+
const x1: [T] extends [number]
223+
? ([T] extends [string] ? { y: number } : { a: number })
224+
: ([T] extends [string] ? { y: number } : { b: number })
225+
= x;
226+
// Infer type parameters: no good
227+
const o2: [T] extends [[infer U]] ? U : { b: number } = o;
228+
~~
229+
!!! error TS2322: Type '{ a: number; b: number; }' is not assignable to type '[T] extends [[infer U]] ? U : { b: number; }'.
230+
// Distributive where T might instantiate to never: no good
231+
const o3: Distributive<T> = o;
232+
~~
233+
!!! error TS2322: Type '{ a: number; b: number; }' is not assignable to type 'Distributive<T>'.
234+
// Distributive where T & string might instantiate to never: also no good
235+
const o4: Distributive<T & string> = o;
236+
~~
237+
!!! error TS2322: Type '{ a: number; b: number; }' is not assignable to type 'Distributive<T & string>'.
238+
// Distributive where {a: T} cannot instantiate to never: OK
239+
const o5: Distributive<{ a: T }> = o;
240+
// 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
241+
const o6: Distributive<[T] extends [never] ? { a: number } : never> = o;
242+
~~
243+
!!! error TS2322: Type '{ a: number; b: number; }' is not assignable to type 'Distributive<[T] extends [never] ? { a: number; } : never>'.
244+
}
209245

tests/baselines/reference/conditionalTypes2.js

+50
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,30 @@ type B2<T, V> =
145145

146146
type C2<T, V, E> =
147147
T extends object ? { [Q in keyof T]: C2<T[Q], V, E>; } : T;
148+
149+
// #26933
150+
type Distributive<T> = T extends { a: number } ? { a: number } : { b: number };
151+
function testAssignabilityToConditionalType<T>() {
152+
const o = { a: 1, b: 2 };
153+
const x: [T] extends [string] ? { y: number } : { a: number, b: number } = undefined!;
154+
// Simple case: OK
155+
const o1: [T] extends [number] ? { a: number } : { b: number } = o;
156+
// Simple case where source happens to be a conditional type: also OK
157+
const x1: [T] extends [number]
158+
? ([T] extends [string] ? { y: number } : { a: number })
159+
: ([T] extends [string] ? { y: number } : { b: number })
160+
= x;
161+
// Infer type parameters: no good
162+
const o2: [T] extends [[infer U]] ? U : { b: number } = o;
163+
// Distributive where T might instantiate to never: no good
164+
const o3: Distributive<T> = o;
165+
// Distributive where T & string might instantiate to never: also no good
166+
const o4: Distributive<T & string> = o;
167+
// Distributive where {a: T} cannot instantiate to never: OK
168+
const o5: Distributive<{ a: T }> = o;
169+
// 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
170+
const o6: Distributive<[T] extends [never] ? { a: number } : never> = o;
171+
}
148172

149173

150174
//// [conditionalTypes2.js]
@@ -222,6 +246,24 @@ function foo(value) {
222246
toString2(value);
223247
}
224248
}
249+
function testAssignabilityToConditionalType() {
250+
var o = { a: 1, b: 2 };
251+
var x = undefined;
252+
// Simple case: OK
253+
var o1 = o;
254+
// Simple case where source happens to be a conditional type: also OK
255+
var x1 = x;
256+
// Infer type parameters: no good
257+
var o2 = o;
258+
// Distributive where T might instantiate to never: no good
259+
var o3 = o;
260+
// Distributive where T & string might instantiate to never: also no good
261+
var o4 = o;
262+
// Distributive where {a: T} cannot instantiate to never: OK
263+
var o5 = o;
264+
// 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
265+
var o6 = o;
266+
}
225267

226268

227269
//// [conditionalTypes2.d.ts]
@@ -304,3 +346,11 @@ declare type B2<T, V> = T extends object ? T extends any[] ? T : {
304346
declare type C2<T, V, E> = T extends object ? {
305347
[Q in keyof T]: C2<T[Q], V, E>;
306348
} : T;
349+
declare type Distributive<T> = T extends {
350+
a: number;
351+
} ? {
352+
a: number;
353+
} : {
354+
b: number;
355+
};
356+
declare function testAssignabilityToConditionalType<T>(): void;

0 commit comments

Comments
 (0)