Skip to content

Commit f590103

Browse files
committed
Stronger guarantees on edge case handling, stolen tests from microsoft#27589
1 parent 8577f7d commit f590103

12 files changed

+465
-44
lines changed

src/compiler/checker.ts

+112-29
Original file line numberDiff line numberDiff line change
@@ -662,6 +662,7 @@ namespace ts {
662662
ImmediateBaseConstraint,
663663
EnumTagType,
664664
JSDocTypeReference,
665+
NeverInstantiable,
665666
}
666667

667668
const enum CheckMode {
@@ -4520,6 +4521,8 @@ namespace ts {
45204521
return !!(<Type>target).immediateBaseConstraint;
45214522
case TypeSystemPropertyName.JSDocTypeReference:
45224523
return !!getSymbolLinks(target as Symbol).resolvedJSDocType;
4524+
case TypeSystemPropertyName.NeverInstantiable:
4525+
return typeof (target as Type).instantiableToNever !== "undefined";
45234526
}
45244527
return Debug.assertNever(propertyName);
45254528
}
@@ -11126,6 +11129,12 @@ namespace ts {
1112611129
!t.numberIndexInfo;
1112711130
}
1112811131

11132+
function isKeylessResolvedType(t: ResolvedType) {
11133+
return t.properties.length === 0 &&
11134+
!t.stringIndexInfo &&
11135+
!t.numberIndexInfo;
11136+
}
11137+
1112911138
function isEmptyObjectType(type: Type): boolean {
1113011139
return type.flags & TypeFlags.Object ? isEmptyResolvedType(resolveStructuredTypeMembers(<ObjectType>type)) :
1113111140
type.flags & TypeFlags.NonPrimitive ? true :
@@ -11134,6 +11143,14 @@ namespace ts {
1113411143
false;
1113511144
}
1113611145

11146+
function isKeylessObjectType(type: Type): boolean {
11147+
return type.flags & TypeFlags.Object ? isKeylessResolvedType(resolveStructuredTypeMembers(<ObjectType>type)) :
11148+
type.flags & TypeFlags.NonPrimitive ? true :
11149+
type.flags & TypeFlags.Union ? some((<UnionType>type).types, isKeylessObjectType) :
11150+
type.flags & TypeFlags.Intersection ? every((<UnionType>type).types, isKeylessObjectType) :
11151+
false;
11152+
}
11153+
1113711154
function isEnumTypeRelatedTo(sourceSymbol: Symbol, targetSymbol: Symbol, errorReporter?: ErrorReporter) {
1113811155
if (sourceSymbol === targetSymbol) {
1113911156
return true;
@@ -11854,6 +11871,70 @@ namespace ts {
1185411871
return relation === definitelyAssignableRelation ? undefined : getConstraintOfType(type);
1185511872
}
1185611873

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

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

src/compiler/types.ts

+2
Original file line numberDiff line numberDiff line change
@@ -3894,6 +3894,8 @@ namespace ts {
38943894
wildcardInstantiation?: Type; // Instantiation with type parameters mapped to wildcard type
38953895
/* @internal */
38963896
immediateBaseConstraint?: Type; // Immediate base constraint cache
3897+
/* @internal */
3898+
instantiableToNever?: boolean; // Flag set by `canBeNever`
38973899
}
38983900

38993901
/* @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)