Skip to content

Commit

Permalink
Finally add that missing relationship allowing a type to be assignabl…
Browse files Browse the repository at this point in the history
…e to both branches of a conditional
  • Loading branch information
weswigham committed Mar 29, 2019
1 parent bb5eb02 commit 7344f40
Show file tree
Hide file tree
Showing 6 changed files with 1,185 additions and 0 deletions.
36 changes: 36 additions & 0 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12836,6 +12836,42 @@ namespace ts {
}
}
}
else if (target.flags & TypeFlags.Conditional) {
const c = target as ConditionalType;
let skipFalse = false;
let skipTrue = false;
// Check if the conditional is always true or always false but still deferred for distribution purposes
if (!isTypeAssignableTo(getPermissiveInstantiation(c.checkType), getPermissiveInstantiation(c.extendsType))) {
skipTrue = true;
}
else if (isTypeAssignableTo(getRestrictiveInstantiation(c.checkType), getRestrictiveInstantiation(c.extendsType))) {
skipFalse = true;
}

// Instantiate with a replacement mapper if the conditional is distributive, replacing the check type with a clone of itself,
// this way {x: string | number, y: string | number} -> (T extends T ? { x: T, y: T } : never) appropriately _fails_ when
// T = string | number (since that will end up distributing and producing `{x: string, y: string} | {x: number, y: number}`,
// to which `{x: string | number, y: string | number}` isn't assignable)
let distributionMapper: TypeMapper | undefined;
const checkVar = getActualTypeVariable(c.root.checkType);
if (c.root.isDistributive && checkVar.flags & TypeFlags.TypeParameter) {
const newParam = cloneTypeParameter(checkVar);
distributionMapper = createReplacementMapper(checkVar, newParam, c.mapper || identityMapper);
newParam.mapper = distributionMapper;
}

// TODO: Find a nice way to include potential conditional type breakdowns in error output, if those seems good (they usually don't)
let localResult: Ternary | undefined;
if (skipTrue || (localResult = isRelatedTo(source, instantiateType(c.trueType, distributionMapper), /*reportErrors*/ false))) {
if (!skipFalse) {
localResult = (localResult || -1) & isRelatedTo(source, instantiateType(c.falseType, distributionMapper), /*reportErrors*/ false);
}
}
if (localResult) {
errorInfo = saveErrorInfo;
return localResult;
}
}

if (source.flags & TypeFlags.TypeVariable) {
if (source.flags & TypeFlags.IndexedAccess && target.flags & TypeFlags.IndexedAccess) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
tests/cases/compiler/conditionalTypeAssignabilityWhenDeferred.ts(28,20): error TS2345: Argument of type 'string' is not assignable to parameter of type 'never'.
tests/cases/compiler/conditionalTypeAssignabilityWhenDeferred.ts(29,21): error TS2345: Argument of type 'string' is not assignable to parameter of type 'never'.
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'.
tests/cases/compiler/conditionalTypeAssignabilityWhenDeferred.ts(46,3): error TS2322: Type 'string' is not assignable to type 'Foo<T>'.
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; }'.
tests/cases/compiler/conditionalTypeAssignabilityWhenDeferred.ts(95,23): error TS2345: Argument of type '"hi"' is not assignable to parameter of type 'Unwrap<this["prop"]>'.
tests/cases/compiler/conditionalTypeAssignabilityWhenDeferred.ts(106,3): error TS2322: Type 'Q' is not assignable to type 'InferBecauseWhyNot<Q>'.
Type '(arg: any) => any' is not assignable to type 'InferBecauseWhyNot<Q>'.
tests/cases/compiler/conditionalTypeAssignabilityWhenDeferred.ts(116,3): error TS2322: Type 'Q' is not assignable to type 'InferBecauseWhyNotDistributive<Q>'.
Type '(arg: any) => any' is not assignable to type 'InferBecauseWhyNotDistributive<Q>'.


==== tests/cases/compiler/conditionalTypeAssignabilityWhenDeferred.ts (8 errors) ====
export type FilterPropsByType<T, TT> = {
[K in keyof T]: T[K] extends TT ? K : never
}[keyof T];

function select<
T extends string | number,
TList extends object,
TValueProp extends FilterPropsByType<TList, T>
>(property: T, list: TList[], valueProp: TValueProp) {}

export function func<XX extends string>(x: XX, tipos: { value: XX }[]) {
select(x, tipos, "value");
}

declare function onlyNullablePlease<T extends null extends T ? any : never>(
value: T
): void;

declare function onlyNullablePlease2<
T extends [null] extends [T] ? any : never
>(value: T): void;

declare var z: string | null;
onlyNullablePlease(z); // works as expected
onlyNullablePlease2(z); // works as expected

declare var y: string;
onlyNullablePlease(y); // error as expected
~
!!! error TS2345: Argument of type 'string' is not assignable to parameter of type 'never'.
onlyNullablePlease2(y); // error as expected
~
!!! error TS2345: Argument of type 'string' is not assignable to parameter of type 'never'.

function f<T>(t: T) {
var x: T | null = Math.random() > 0.5 ? null : t;
onlyNullablePlease(x); // should work
onlyNullablePlease2(x); // should work
}

function f2<T>(t1: { x: T; y: T }, t2: T extends T ? { x: T; y: T } : never) {
t1 = t2; // OK
t2 = t1; // should fail
~~
!!! error TS2322: Type '{ x: T; y: T; }' is not assignable to type 'T extends T ? { x: T; y: T; } : never'.
}

type Foo<T> = T extends true ? string : "a";

function test<T>(x: Foo<T>, s: string) {
x = "a"; // Currently an error, should be ok
x = s; // Error
~
!!! error TS2322: Type 'string' is not assignable to type 'Foo<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; }'.

// The next 4 are arguable - if you choose to ignore the `never` distribution case,
// then they're all good. The `never` case _is_ a bit of an outlier - we say distributive types
// look approximately like the sum of their branches, but the `never` case bucks that.
// There's an argument for the result of dumping `never` into a distributive conditional
// being not `never`, but instead the intersection of the branches - a much more precise bound
// on that "impossible" input.

// 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;
}

type Wrapped<T> = { ___secret: T };
type Unwrap<T> = T extends Wrapped<infer U> ? U : T;

declare function set<T, K extends keyof T>(
obj: T,
key: K,
value: Unwrap<T[K]>
): Unwrap<T[K]>;

class Foo2 {
prop!: Wrapped<string>;

method() {
set(this, "prop", "hi"); // <-- type error
~~~~
!!! error TS2345: Argument of type '"hi"' is not assignable to parameter of type 'Unwrap<this["prop"]>'.
}
}

set(new Foo2(), "prop", "hi"); // <-- typechecks

type InferBecauseWhyNot<T> = [T] extends [(p: infer P1) => any]
? P1 | T
: never;

function f3<Q extends (arg: any) => any>(x: Q): InferBecauseWhyNot<Q> {
return x;
~~~~~~~~~
!!! error TS2322: Type 'Q' is not assignable to type 'InferBecauseWhyNot<Q>'.
!!! error TS2322: Type '(arg: any) => any' is not assignable to type 'InferBecauseWhyNot<Q>'.
}

type InferBecauseWhyNotDistributive<T> = T extends (p: infer P1) => any
? P1 | T
: never;

function f4<Q extends (arg: any) => any>(
x: Q
): InferBecauseWhyNotDistributive<Q> {
return x; // should fail
~~~~~~~~~
!!! error TS2322: Type 'Q' is not assignable to type 'InferBecauseWhyNotDistributive<Q>'.
!!! error TS2322: Type '(arg: any) => any' is not assignable to type 'InferBecauseWhyNotDistributive<Q>'.
}

184 changes: 184 additions & 0 deletions tests/baselines/reference/conditionalTypeAssignabilityWhenDeferred.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
//// [conditionalTypeAssignabilityWhenDeferred.ts]
export type FilterPropsByType<T, TT> = {
[K in keyof T]: T[K] extends TT ? K : never
}[keyof T];

function select<
T extends string | number,
TList extends object,
TValueProp extends FilterPropsByType<TList, T>
>(property: T, list: TList[], valueProp: TValueProp) {}

export function func<XX extends string>(x: XX, tipos: { value: XX }[]) {
select(x, tipos, "value");
}

declare function onlyNullablePlease<T extends null extends T ? any : never>(
value: T
): void;

declare function onlyNullablePlease2<
T extends [null] extends [T] ? any : never
>(value: T): void;

declare var z: string | null;
onlyNullablePlease(z); // works as expected
onlyNullablePlease2(z); // works as expected

declare var y: string;
onlyNullablePlease(y); // error as expected
onlyNullablePlease2(y); // error as expected

function f<T>(t: T) {
var x: T | null = Math.random() > 0.5 ? null : t;
onlyNullablePlease(x); // should work
onlyNullablePlease2(x); // should work
}

function f2<T>(t1: { x: T; y: T }, t2: T extends T ? { x: T; y: T } : never) {
t1 = t2; // OK
t2 = t1; // should fail
}

type Foo<T> = T extends true ? string : "a";

function test<T>(x: Foo<T>, s: string) {
x = "a"; // Currently an error, should be ok
x = s; // Error
}

// #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;

// The next 4 are arguable - if you choose to ignore the `never` distribution case,
// then they're all good. The `never` case _is_ a bit of an outlier - we say distributive types
// look approximately like the sum of their branches, but the `never` case bucks that.
// There's an argument for the result of dumping `never` into a distributive conditional
// being not `never`, but instead the intersection of the branches - a much more precise bound
// on that "impossible" input.

// 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;
}

type Wrapped<T> = { ___secret: T };
type Unwrap<T> = T extends Wrapped<infer U> ? U : T;

declare function set<T, K extends keyof T>(
obj: T,
key: K,
value: Unwrap<T[K]>
): Unwrap<T[K]>;

class Foo2 {
prop!: Wrapped<string>;

method() {
set(this, "prop", "hi"); // <-- type error
}
}

set(new Foo2(), "prop", "hi"); // <-- typechecks

type InferBecauseWhyNot<T> = [T] extends [(p: infer P1) => any]
? P1 | T
: never;

function f3<Q extends (arg: any) => any>(x: Q): InferBecauseWhyNot<Q> {
return x;
}

type InferBecauseWhyNotDistributive<T> = T extends (p: infer P1) => any
? P1 | T
: never;

function f4<Q extends (arg: any) => any>(
x: Q
): InferBecauseWhyNotDistributive<Q> {
return x; // should fail
}


//// [conditionalTypeAssignabilityWhenDeferred.js]
"use strict";
exports.__esModule = true;
function select(property, list, valueProp) { }
function func(x, tipos) {
select(x, tipos, "value");
}
exports.func = func;
onlyNullablePlease(z); // works as expected
onlyNullablePlease2(z); // works as expected
onlyNullablePlease(y); // error as expected
onlyNullablePlease2(y); // error as expected
function f(t) {
var x = Math.random() > 0.5 ? null : t;
onlyNullablePlease(x); // should work
onlyNullablePlease2(x); // should work
}
function f2(t1, t2) {
t1 = t2; // OK
t2 = t1; // should fail
}
function test(x, s) {
x = "a"; // Currently an error, should be ok
x = s; // Error
}
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;
// The next 4 are arguable - if you choose to ignore the `never` distribution case,
// then they're all good. The `never` case _is_ a bit of an outlier - we say distributive types
// look approximately like the sum of their branches, but the `never` case bucks that.
// There's an argument for the result of dumping `never` into a distributive conditional
// being not `never`, but instead the intersection of the branches - a much more precise bound
// on that "impossible" input.
// 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;
}
var Foo2 = /** @class */ (function () {
function Foo2() {
}
Foo2.prototype.method = function () {
set(this, "prop", "hi"); // <-- type error
};
return Foo2;
}());
set(new Foo2(), "prop", "hi"); // <-- typechecks
function f3(x) {
return x;
}
function f4(x) {
return x; // should fail
}
Loading

0 comments on commit 7344f40

Please sign in to comment.