Typescript: Constraints are not considered for Conditional Types

Created on 24 Apr 2019  路  5Comments  路  Source: microsoft/TypeScript

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.

Playground Link

Design Limitation

Most helpful comment

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

  1. string <: any and any <: number, but it's not the case that string <: number.
  2. number <: Object and Object <: object, but it's not the case that number <: object.
  3. { 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 }.
  4. Callbacks with void:

    • () => unknown <: () => void and () => void <: () => (boolean | void),

    • but it's not the case that () => unknown <: () => (boolean | void).

All 5 comments

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

  1. string <: any and any <: number, but it's not the case that string <: number.
  2. number <: Object and Object <: object, but it's not the case that number <: object.
  3. { 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 }.
  4. Callbacks with void:

    • () => unknown <: () => void and () => void <: () => (boolean | void),

    • but it's not the case that () => 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
Was this page helpful?
0 / 5 - 0 ratings

Related issues

manekinekko picture manekinekko  路  3Comments

jbondc picture jbondc  路  3Comments

zhuravlikjb picture zhuravlikjb  路  3Comments

uber5001 picture uber5001  路  3Comments

blendsdk picture blendsdk  路  3Comments