Typescript: Incorrect type inference

Created on 3 Jun 2019  路  6Comments  路  Source: microsoft/TypeScript


TypeScript Version: 3.3.3333


Search Terms:
typescript type guard else never

Code

    enum Type { N, S }

    interface Interface<E extends Type | unknown = unknown> {
      type?: Type
      data?: // just for an idea of how the E parameter is used
        E extends Type.N ? number :
        E extends Type.S ? string :
        number | string
    }

    const isN = (i: Interface): i is Interface<Type.N> => i.type === Type.N

    const i: Interface<unknown> = {}

    if (isN(i)) {
      // i is Interface<Type.N>
    } else {
      // i is never
      // shouldn't it stay Interface<unknown>?
      i
    }

Related Issues:
https://github.com/microsoft/TypeScript/issues/17789
https://github.com/Microsoft/TypeScript/issues/10934

Design Limitation

Most helpful comment

The value v is narrowed on assignment, so while it is declared has having type Type, it actually has type number. From that it follows that isN(v) is always true and never false, hence the narrowed type you see.

If you adjust to declaration of v to prevent narrowing you get the expected behaviour.

const v: Type = 42 as Type;

if (isN(v)) {
    const n: number = v;
} else {
    const s: string = v;
}

All 6 comments

The value v is narrowed on assignment, so while it is declared has having type Type, it actually has type number. From that it follows that isN(v) is always true and never false, hence the narrowed type you see.

If you adjust to declaration of v to prevent narrowing you get the expected behaviour.

const v: Type = 42 as Type;

if (isN(v)) {
    const n: number = v;
} else {
    const s: string = v;
}

Note that the complex types here are just a distraction; it reproduces fine with const v: string | number = 42

Thanks for the quick response, guys! Unfortunately, I tried to create a minimal example based on a much more complex code, where the issue has originated, I guess it became too simple to reproduce ) I've updated the example with a little bit more complexity.

Ah ok. Yes I think something is not right here. The checker thinks that Interface<unknown> is a subtype of Interface<Type.N> which is why it's getting filtered from the type in the else clause.

const i: Interface<unknown> = { data: "hello" };
let j: Interface<Type.N> = { data: 3 }
j.data = i.data; // error
j = i; // no error

This is likely related to #31295 because you get the correct behaviour if you use two identical but differently named aliases.

enum Type { N, S }

interface Interface<E extends Type | unknown = unknown> {
  type?: Type
  data?: // just for an idea of how the E parameter is used
  E extends Type.N ? number :
  E extends Type.S ? string :
  number | string
}

interface Interface2<E extends Type | unknown = unknown> {
  type?: Type
  data?: // just for an idea of how the E parameter is used
  E extends Type.N ? number :
  E extends Type.S ? string :
  number | string
}

const isN = (i: Interface): i is Interface2<Type.N> => i.type === Type.N;
const i: Interface<unknown> = { data: "hello" };
let j: Interface2<Type.N> = { data: 3 }
j.data = i.data; // error
j = i; // error

if (isN(i)) {
  i // i is Interface2<Type.N>
} else {
  i // i is Interface<unknown>
}

This is an effect of the type checker deciding that Interface<E> bi-variant in E (because of the way E is witnessed only in the test of a conditional type). We measure variance in order to avoid repeated structural type checks, but there are known limitations around non-linear types such as the conditional type in the example. So, this is basically a design limitation.

You could consider including an optional property that witnesses E in the interface:

interface Interface<E = unknown> {
    // ...
    __E__?: E;
}

This causes the type checker to measure the interface as co-variant in E and you then get the expected behavior.

Awesome, @ahejlsberg, thanks for your help!

Was this page helpful?
0 / 5 - 0 ratings

Related issues

wmaurer picture wmaurer  路  3Comments

dlaberge picture dlaberge  路  3Comments

blendsdk picture blendsdk  路  3Comments

bgrieder picture bgrieder  路  3Comments

Antony-Jones picture Antony-Jones  路  3Comments