Skip to content

Commit 2643e65

Browse files
weswighamsandersn
andauthored
Add missing relationship allowing a type to be assignable to a conditional when assignable to both branches (#30639)
* Finally add that missing relationship allowing a type to be assignable to both branches of a conditional * Explicitly write out Ternary.Maybe * Add slightly modified example from #25413 * fix sick sentence * Loosen check to skip false branch constraint check to consider `infer` parameters as always satisfied in the extends clause * Simplify things a bit, only instantiate once Co-authored-by: Nathan Shively-Sanders <293473+sandersn@users.noreply.github.com>
1 parent b2d1f53 commit 2643e65

14 files changed

+5727
-0
lines changed

src/compiler/checker.ts

+37
Original file line numberDiff line numberDiff line change
@@ -14607,6 +14607,13 @@ namespace ts {
1460714607
return type[cache] = type;
1460814608
}
1460914609

14610+
function isConditionalTypeAlwaysTrueDisregardingInferTypes(type: ConditionalType) {
14611+
const extendsInferParamMapper = type.root.inferTypeParameters && createTypeMapper(type.root.inferTypeParameters, map(type.root.inferTypeParameters, () => wildcardType));
14612+
const checkType = type.checkType;
14613+
const extendsType = type.extendsType;
14614+
return isTypeAssignableTo(getRestrictiveInstantiation(checkType), getRestrictiveInstantiation(instantiateType(extendsType, extendsInferParamMapper)));
14615+
}
14616+
1461014617
function getSimplifiedConditionalType(type: ConditionalType, writing: boolean) {
1461114618
const checkType = type.checkType;
1461214619
const extendsType = type.extendsType;
@@ -18139,6 +18146,36 @@ namespace ts {
1813918146
}
1814018147
}
1814118148
}
18149+
else if (target.flags & TypeFlags.Conditional) {
18150+
const c = target as ConditionalType;
18151+
// Check if the conditional is always true or always false but still deferred for distribution purposes
18152+
const skipTrue = !isTypeAssignableTo(getPermissiveInstantiation(c.checkType), getPermissiveInstantiation(c.extendsType));
18153+
const skipFalse = !skipTrue && isConditionalTypeAlwaysTrueDisregardingInferTypes(c);
18154+
18155+
// Instantiate with a replacement mapper if the conditional is distributive, replacing the check type with a clone of itself,
18156+
// this way {x: string | number, y: string | number} -> (T extends T ? { x: T, y: T } : never) appropriately _fails_ when
18157+
// T = string | number (since that will end up distributing and producing `{x: string, y: string} | {x: number, y: number}`,
18158+
// to which `{x: string | number, y: string | number}` isn't assignable)
18159+
let distributionMapper: TypeMapper | undefined;
18160+
const checkVar = getActualTypeVariable(c.root.checkType);
18161+
if (c.root.isDistributive && checkVar.flags & TypeFlags.TypeParameter) {
18162+
const newParam = cloneTypeParameter(checkVar);
18163+
distributionMapper = prependTypeMapping(checkVar, newParam, c.mapper);
18164+
newParam.mapper = distributionMapper;
18165+
}
18166+
18167+
// TODO: Find a nice way to include potential conditional type breakdowns in error output, if they seem good (they usually don't)
18168+
let localResult: Ternary | undefined;
18169+
if (skipTrue || (localResult = isRelatedTo(source, distributionMapper ? instantiateType(getTypeFromTypeNode(c.root.node.trueType), distributionMapper) : getTrueTypeFromConditionalType(c), /*reportErrors*/ false))) {
18170+
if (!skipFalse) {
18171+
localResult = (localResult || Ternary.Maybe) & isRelatedTo(source, distributionMapper ? instantiateType(getTypeFromTypeNode(c.root.node.falseType), distributionMapper) : getFalseTypeFromConditionalType(c), /*reportErrors*/ false);
18172+
}
18173+
}
18174+
if (localResult) {
18175+
resetErrorInfo(saveErrorInfo);
18176+
return localResult;
18177+
}
18178+
}
1814218179
else if (target.flags & TypeFlags.TemplateLiteral && source.flags & TypeFlags.StringLiteral) {
1814318180
if (isPatternLiteralType(target)) {
1814418181
// match all non-`string` segments
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
tests/cases/compiler/conditionalTypeAssignabilityWhenDeferred.ts(28,20): error TS2345: Argument of type 'string' is not assignable to parameter of type 'never'.
2+
tests/cases/compiler/conditionalTypeAssignabilityWhenDeferred.ts(29,21): error TS2345: Argument of type 'string' is not assignable to parameter of type 'never'.
3+
tests/cases/compiler/conditionalTypeAssignabilityWhenDeferred.ts(39,3): error TS2322: Type '{ x: T; y: T; }' is not assignable to type 'T extends T ? { x: T; y: T; } : never'.
4+
tests/cases/compiler/conditionalTypeAssignabilityWhenDeferred.ts(46,3): error TS2322: Type 'string' is not assignable to type 'Foo<T>'.
5+
tests/cases/compiler/conditionalTypeAssignabilityWhenDeferred.ts(63,9): error TS2322: Type '{ a: number; b: number; }' is not assignable to type '[T] extends [[infer U]] ? U : { b: number; }'.
6+
tests/cases/compiler/conditionalTypeAssignabilityWhenDeferred.ts(95,23): error TS2345: Argument of type 'string' is not assignable to parameter of type 'Unwrap<this["prop"]>'.
7+
tests/cases/compiler/conditionalTypeAssignabilityWhenDeferred.ts(106,3): error TS2322: Type 'Q' is not assignable to type 'InferBecauseWhyNot<Q>'.
8+
Type '(arg: any) => any' is not assignable to type 'InferBecauseWhyNot<Q>'.
9+
tests/cases/compiler/conditionalTypeAssignabilityWhenDeferred.ts(116,3): error TS2322: Type 'Q' is not assignable to type 'InferBecauseWhyNotDistributive<Q>'.
10+
Type '(arg: any) => any' is not assignable to type 'InferBecauseWhyNotDistributive<Q>'.
11+
12+
13+
==== tests/cases/compiler/conditionalTypeAssignabilityWhenDeferred.ts (8 errors) ====
14+
export type FilterPropsByType<T, TT> = {
15+
[K in keyof T]: T[K] extends TT ? K : never
16+
}[keyof T];
17+
18+
function select<
19+
T extends string | number,
20+
TList extends object,
21+
TValueProp extends FilterPropsByType<TList, T>
22+
>(property: T, list: TList[], valueProp: TValueProp) {}
23+
24+
export function func<XX extends string>(x: XX, tipos: { value: XX }[]) {
25+
select(x, tipos, "value");
26+
}
27+
28+
declare function onlyNullablePlease<T extends null extends T ? any : never>(
29+
value: T
30+
): void;
31+
32+
declare function onlyNullablePlease2<
33+
T extends [null] extends [T] ? any : never
34+
>(value: T): void;
35+
36+
declare var z: string | null;
37+
onlyNullablePlease(z); // works as expected
38+
onlyNullablePlease2(z); // works as expected
39+
40+
declare var y: string;
41+
onlyNullablePlease(y); // error as expected
42+
~
43+
!!! error TS2345: Argument of type 'string' is not assignable to parameter of type 'never'.
44+
onlyNullablePlease2(y); // error as expected
45+
~
46+
!!! error TS2345: Argument of type 'string' is not assignable to parameter of type 'never'.
47+
48+
function f<T>(t: T) {
49+
var x: T | null = Math.random() > 0.5 ? null : t;
50+
onlyNullablePlease(x); // should work
51+
onlyNullablePlease2(x); // should work
52+
}
53+
54+
function f2<T>(t1: { x: T; y: T }, t2: T extends T ? { x: T; y: T } : never) {
55+
t1 = t2; // OK
56+
t2 = t1; // should fail
57+
~~
58+
!!! error TS2322: Type '{ x: T; y: T; }' is not assignable to type 'T extends T ? { x: T; y: T; } : never'.
59+
}
60+
61+
type Foo<T> = T extends true ? string : "a";
62+
63+
function test<T>(x: Foo<T>, s: string) {
64+
x = "a"; // Currently an error, should be ok
65+
x = s; // Error
66+
~
67+
!!! error TS2322: Type 'string' is not assignable to type 'Foo<T>'.
68+
}
69+
70+
// #26933
71+
type Distributive<T> = T extends { a: number } ? { a: number } : { b: number };
72+
function testAssignabilityToConditionalType<T>() {
73+
const o = { a: 1, b: 2 };
74+
const x: [T] extends [string]
75+
? { y: number }
76+
: { a: number; b: number } = undefined!;
77+
// Simple case: OK
78+
const o1: [T] extends [number] ? { a: number } : { b: number } = o;
79+
// Simple case where source happens to be a conditional type: also OK
80+
const x1: [T] extends [number]
81+
? ([T] extends [string] ? { y: number } : { a: number })
82+
: ([T] extends [string] ? { y: number } : { b: number }) = x;
83+
// Infer type parameters: no good
84+
const o2: [T] extends [[infer U]] ? U : { b: number } = o;
85+
~~
86+
!!! error TS2322: Type '{ a: number; b: number; }' is not assignable to type '[T] extends [[infer U]] ? U : { b: number; }'.
87+
88+
// The next 4 are arguable - if you choose to ignore the `never` distribution case,
89+
// then they're all good. The `never` case _is_ a bit of an outlier - we say distributive types
90+
// look approximately like the sum of their branches, but the `never` case bucks that.
91+
// There's an argument for the result of dumping `never` into a distributive conditional
92+
// being not `never`, but instead the intersection of the branches - a much more precise bound
93+
// on that "impossible" input.
94+
95+
// Distributive where T might instantiate to never: no good
96+
const o3: Distributive<T> = o;
97+
// Distributive where T & string might instantiate to never: also no good
98+
const o4: Distributive<T & string> = o;
99+
// Distributive where {a: T} cannot instantiate to never: OK
100+
const o5: Distributive<{ a: T }> = o;
101+
// 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
102+
const o6: Distributive<[T] extends [never] ? { a: number } : never> = o;
103+
}
104+
105+
type Wrapped<T> = { ___secret: T };
106+
type Unwrap<T> = T extends Wrapped<infer U> ? U : T;
107+
108+
declare function set<T, K extends keyof T>(
109+
obj: T,
110+
key: K,
111+
value: Unwrap<T[K]>
112+
): Unwrap<T[K]>;
113+
114+
class Foo2 {
115+
prop!: Wrapped<string>;
116+
117+
method() {
118+
set(this, "prop", "hi"); // <-- type error
119+
~~~~
120+
!!! error TS2345: Argument of type 'string' is not assignable to parameter of type 'Unwrap<this["prop"]>'.
121+
}
122+
}
123+
124+
set(new Foo2(), "prop", "hi"); // <-- typechecks
125+
126+
type InferBecauseWhyNot<T> = [T] extends [(p: infer P1) => any]
127+
? P1 | T
128+
: never;
129+
130+
function f3<Q extends (arg: any) => any>(x: Q): InferBecauseWhyNot<Q> {
131+
return x;
132+
~~~~~~~~~
133+
!!! error TS2322: Type 'Q' is not assignable to type 'InferBecauseWhyNot<Q>'.
134+
!!! error TS2322: Type '(arg: any) => any' is not assignable to type 'InferBecauseWhyNot<Q>'.
135+
}
136+
137+
type InferBecauseWhyNotDistributive<T> = T extends (p: infer P1) => any
138+
? P1 | T
139+
: never;
140+
141+
function f4<Q extends (arg: any) => any>(
142+
x: Q
143+
): InferBecauseWhyNotDistributive<Q> {
144+
return x; // should fail
145+
~~~~~~~~~
146+
!!! error TS2322: Type 'Q' is not assignable to type 'InferBecauseWhyNotDistributive<Q>'.
147+
!!! error TS2322: Type '(arg: any) => any' is not assignable to type 'InferBecauseWhyNotDistributive<Q>'.
148+
}
149+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
//// [conditionalTypeAssignabilityWhenDeferred.ts]
2+
export type FilterPropsByType<T, TT> = {
3+
[K in keyof T]: T[K] extends TT ? K : never
4+
}[keyof T];
5+
6+
function select<
7+
T extends string | number,
8+
TList extends object,
9+
TValueProp extends FilterPropsByType<TList, T>
10+
>(property: T, list: TList[], valueProp: TValueProp) {}
11+
12+
export function func<XX extends string>(x: XX, tipos: { value: XX }[]) {
13+
select(x, tipos, "value");
14+
}
15+
16+
declare function onlyNullablePlease<T extends null extends T ? any : never>(
17+
value: T
18+
): void;
19+
20+
declare function onlyNullablePlease2<
21+
T extends [null] extends [T] ? any : never
22+
>(value: T): void;
23+
24+
declare var z: string | null;
25+
onlyNullablePlease(z); // works as expected
26+
onlyNullablePlease2(z); // works as expected
27+
28+
declare var y: string;
29+
onlyNullablePlease(y); // error as expected
30+
onlyNullablePlease2(y); // error as expected
31+
32+
function f<T>(t: T) {
33+
var x: T | null = Math.random() > 0.5 ? null : t;
34+
onlyNullablePlease(x); // should work
35+
onlyNullablePlease2(x); // should work
36+
}
37+
38+
function f2<T>(t1: { x: T; y: T }, t2: T extends T ? { x: T; y: T } : never) {
39+
t1 = t2; // OK
40+
t2 = t1; // should fail
41+
}
42+
43+
type Foo<T> = T extends true ? string : "a";
44+
45+
function test<T>(x: Foo<T>, s: string) {
46+
x = "a"; // Currently an error, should be ok
47+
x = s; // Error
48+
}
49+
50+
// #26933
51+
type Distributive<T> = T extends { a: number } ? { a: number } : { b: number };
52+
function testAssignabilityToConditionalType<T>() {
53+
const o = { a: 1, b: 2 };
54+
const x: [T] extends [string]
55+
? { y: number }
56+
: { a: number; b: number } = undefined!;
57+
// Simple case: OK
58+
const o1: [T] extends [number] ? { a: number } : { b: number } = o;
59+
// Simple case where source happens to be a conditional type: also OK
60+
const x1: [T] extends [number]
61+
? ([T] extends [string] ? { y: number } : { a: number })
62+
: ([T] extends [string] ? { y: number } : { b: number }) = x;
63+
// Infer type parameters: no good
64+
const o2: [T] extends [[infer U]] ? U : { b: number } = o;
65+
66+
// The next 4 are arguable - if you choose to ignore the `never` distribution case,
67+
// then they're all good. The `never` case _is_ a bit of an outlier - we say distributive types
68+
// look approximately like the sum of their branches, but the `never` case bucks that.
69+
// There's an argument for the result of dumping `never` into a distributive conditional
70+
// being not `never`, but instead the intersection of the branches - a much more precise bound
71+
// on that "impossible" input.
72+
73+
// Distributive where T might instantiate to never: no good
74+
const o3: Distributive<T> = o;
75+
// Distributive where T & string might instantiate to never: also no good
76+
const o4: Distributive<T & string> = o;
77+
// Distributive where {a: T} cannot instantiate to never: OK
78+
const o5: Distributive<{ a: T }> = o;
79+
// 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
80+
const o6: Distributive<[T] extends [never] ? { a: number } : never> = o;
81+
}
82+
83+
type Wrapped<T> = { ___secret: T };
84+
type Unwrap<T> = T extends Wrapped<infer U> ? U : T;
85+
86+
declare function set<T, K extends keyof T>(
87+
obj: T,
88+
key: K,
89+
value: Unwrap<T[K]>
90+
): Unwrap<T[K]>;
91+
92+
class Foo2 {
93+
prop!: Wrapped<string>;
94+
95+
method() {
96+
set(this, "prop", "hi"); // <-- type error
97+
}
98+
}
99+
100+
set(new Foo2(), "prop", "hi"); // <-- typechecks
101+
102+
type InferBecauseWhyNot<T> = [T] extends [(p: infer P1) => any]
103+
? P1 | T
104+
: never;
105+
106+
function f3<Q extends (arg: any) => any>(x: Q): InferBecauseWhyNot<Q> {
107+
return x;
108+
}
109+
110+
type InferBecauseWhyNotDistributive<T> = T extends (p: infer P1) => any
111+
? P1 | T
112+
: never;
113+
114+
function f4<Q extends (arg: any) => any>(
115+
x: Q
116+
): InferBecauseWhyNotDistributive<Q> {
117+
return x; // should fail
118+
}
119+
120+
121+
//// [conditionalTypeAssignabilityWhenDeferred.js]
122+
"use strict";
123+
exports.__esModule = true;
124+
exports.func = void 0;
125+
function select(property, list, valueProp) { }
126+
function func(x, tipos) {
127+
select(x, tipos, "value");
128+
}
129+
exports.func = func;
130+
onlyNullablePlease(z); // works as expected
131+
onlyNullablePlease2(z); // works as expected
132+
onlyNullablePlease(y); // error as expected
133+
onlyNullablePlease2(y); // error as expected
134+
function f(t) {
135+
var x = Math.random() > 0.5 ? null : t;
136+
onlyNullablePlease(x); // should work
137+
onlyNullablePlease2(x); // should work
138+
}
139+
function f2(t1, t2) {
140+
t1 = t2; // OK
141+
t2 = t1; // should fail
142+
}
143+
function test(x, s) {
144+
x = "a"; // Currently an error, should be ok
145+
x = s; // Error
146+
}
147+
function testAssignabilityToConditionalType() {
148+
var o = { a: 1, b: 2 };
149+
var x = undefined;
150+
// Simple case: OK
151+
var o1 = o;
152+
// Simple case where source happens to be a conditional type: also OK
153+
var x1 = x;
154+
// Infer type parameters: no good
155+
var o2 = o;
156+
// The next 4 are arguable - if you choose to ignore the `never` distribution case,
157+
// then they're all good. The `never` case _is_ a bit of an outlier - we say distributive types
158+
// look approximately like the sum of their branches, but the `never` case bucks that.
159+
// There's an argument for the result of dumping `never` into a distributive conditional
160+
// being not `never`, but instead the intersection of the branches - a much more precise bound
161+
// on that "impossible" input.
162+
// Distributive where T might instantiate to never: no good
163+
var o3 = o;
164+
// Distributive where T & string might instantiate to never: also no good
165+
var o4 = o;
166+
// Distributive where {a: T} cannot instantiate to never: OK
167+
var o5 = o;
168+
// 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
169+
var o6 = o;
170+
}
171+
var Foo2 = /** @class */ (function () {
172+
function Foo2() {
173+
}
174+
Foo2.prototype.method = function () {
175+
set(this, "prop", "hi"); // <-- type error
176+
};
177+
return Foo2;
178+
}());
179+
set(new Foo2(), "prop", "hi"); // <-- typechecks
180+
function f3(x) {
181+
return x;
182+
}
183+
function f4(x) {
184+
return x; // should fail
185+
}

0 commit comments

Comments
 (0)