TypeScript Version: 3.5.0-dev.20190424
Code
function f<
TType extends { p: string },
>(
bla: TType
): void {
const x: TType['p'] = 'test'; // typechecks
const y: TType["p"] extends string ? string : number = "test"; // does not
}
Expected behavior:
It typechecks.
Actual behavior:
It does not:
Type '"test"' is not assignable to type 'TType["p"] extends string ? string : number'.
const y: TType["p"] extends string ? string : number
The assignment to x however typechecks.
This is working as intended. Conditional type resolution ignores type parameter constraints to prevent undesirable resolutions that violate transitivity. See this comment:
// Return trueType for a definitely true extends check. We check instantiations of the two
// types with type parameters mapped to their restrictive form, i.e. a form of the type parameter
// that has no constraint. This ensures that, for example, the type
// type Foo<T extends { x: any }> = T extends { x: string } ? string : number
// doesn't immediately resolve to 'string' instead of being deferred.
A concrete example:
function f<
TType extends { p: any },
>(
bla: TType
): TType["p"] extends string ? string : number {
return "test"; // If we used the constraint this would be legal.
}
const res: number = f({ p: 3 }); // res === "test";
There are cases where using the constraint is sound but it involves exploring the constraint type.
I've managed to overcome this issue by replacing conditional types with map-types:
function f<
TType extends { kind: "a" },
>(
bla: TType
): void {
const x: TType['kind'] = 'a'; // typechecks
let y: { a: string, b: number }[TType["kind"]]; // does not
y = "test";
y = 4;
}
@jack-williams So this restriction is only because of any?
So this restriction is only because of any?
In general the root of the issue is that assignability is not a transitive relation, however any is the main culprit. There are four violations of transitivity that I know of; there may be more:
(Thanks to @rkirov for number four).
I use <: for assignability
string <: any and any <: number, but it's not the case that string <: number.number <: Object and Object <: object, but it's not the case that number <: object.{ x: number; y: number } <: { x: number } and { x: number } <: { x: number; y?: string } but it's not the case that { x: number; y: number } <: { x: number; y?: string }.void:() => unknown <: () => void and () => void <: () => (boolean | void), () => unknown <: () => (boolean | void).@jack-williams I ended up here looking for 'transitivity failures', so I got one more for you (no 'any's)
declare let c: {a(): {}|null|void};
declare let b: {a(): void};
declare let a: {a(): unknown};
b = a;
c = b;
// but
c = a; // error
Also that means it is not worth filing a bug, given the amount of other known failures.
Since this seems to be as good a place as any to document transitivity failures and it's not listed yet, there's also some good ones with optional parameters. This one is fun because you can run it both directions.
declare let x: (a?: number) => void;
declare let y: () => void;
declare let z: (a?: string) => void;
y = z;
x = y;
x = z; // error
y = x;
z = y;
z = x; // error
Most helpful comment
In general the root of the issue is that assignability is not a transitive relation, however
anyis the main culprit. There are four violations of transitivity that I know of; there may be more:(Thanks to @rkirov for number four).
I use
<:for assignabilitystring <: anyandany <: number, but it's not the case thatstring <: number.number <: ObjectandObject <: object, but it's not the case thatnumber <: object.{ x: number; y: number } <: { x: number }and{ x: number } <: { x: number; y?: string }but it's not the case that{ x: number; y: number } <: { x: number; y?: string }.void:() => unknown <: () => voidand() => void <: () => (boolean | void),() => unknown <: () => (boolean | void).