Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Type parameter constrained to union cannot be exhaustively narrowed to 'never' #13215

Open
Strate opened this issue Dec 29, 2016 · 11 comments
Open
Labels
In Discussion Not yet reached consensus Suggestion An idea for TypeScript

Comments

@Strate
Copy link

Strate commented Dec 29, 2016

TypeScript Version: 2.1.1 (http://www.typescriptlang.org/play/index.html)

Code

type Type = "a" | "b"

function isA(a: any): a is "a" {
    return a === "a"
}

function isB(a: any): a is "b" {
    return a === "b"
}

function assertNever(arg: never) {
    throw new Error("This should never be called")
}

function handleAction<T extends Type>(type: T) {
    if (isA(type)) {
        return type
    } else if (isB(type)) {
        return type
    } else {
        assertNever(type)
    }
}

Expected behavior:
No compilation error

Actual behavior:
Argument of type 'T' is not assignable to type 'never'

@akarzazi
Copy link

The statement T extends Type means that T is at least Type. Then T may be more than Type.

The generic signature is not relevant here, since Type does not seems extensible.
Use non generic signature :
function handleAction(type: Type) { ...

@Strate
Copy link
Author

Strate commented Dec 29, 2016

@akarzazi this is just simplified example, of course in just this example generic looks like overengineering. In reality this is method of class with a type parameter constrained to Union, and I need cover all possible cases of that union.

If T extends "a"|"b", then T should be either "a", or "b". What do you mean if "T could be more than "a"|"b""?

@akarzazi
Copy link

I don't think you can extends string literal types anyway.
Can you please provide the real code sample ?

@DanielRosenwasser DanielRosenwasser added the Bug A bug in TypeScript label Dec 30, 2016
@DanielRosenwasser DanielRosenwasser changed the title Type parameter, contrained with Union, is not narrowed Type parameter constrained to set of literal types cannot be exhaustively narrowed to 'never' Dec 30, 2016
@DanielRosenwasser DanielRosenwasser added this to the TypeScript 2.2 milestone Dec 30, 2016
@Strate
Copy link
Author

Strate commented Dec 30, 2016

@DanielRosenwasser this case is not only about set of literlal types, this is more generic issue, about any union constraint, for example:

interface Action1 {
    value: {
        specificAction1: boolean
    }
}

interface Action2 {
    value: {
        specificAction2: boolean
    }
}

function isAction1(a: any): a is Action1 {
    return a != null && a.value != null && a.value.specificAction1 != null
}

function isAction2(a: any): a is Action2 {
    return a != null && a.value != null && a.value.specificAction2 != null
}

function assertNever(arg: never) {
    throw new Error("This should never be called")
}

type AnyAction = Action1 | Action2

function handleAction<T extends AnyAction>(action: T) {
    if (isAction1(action)) {
        handleAction1(action)
    } else if (isAction2(action)) {
        handleAction2(action)
    } else {
        assertNever(action)
    }
}

declare function handleAction1(action: Action1)
declare function handleAction2(action: Action2)

has exactly the same issue.

@DanielRosenwasser
Copy link
Member

Sorry about that - I'm never sure of our position on those cases - when you have an object type (e.g. Action1 or Action2), not satisfying a check like isAction1 doesn't necessarily mean that the type does not an Action1. But it seems like the current behavior is to narrow the type out of the negative case.

@DanielRosenwasser DanielRosenwasser changed the title Type parameter constrained to set of literal types cannot be exhaustively narrowed to 'never' Type parameter constrained to union cannot be exhaustively narrowed to 'never' Dec 30, 2016
@akarzazi
Copy link

Why use the generic signature ? (again)
function handleAction(action: AnyAction) {

@mhegazy mhegazy removed this from the TypeScript 2.2 milestone Dec 30, 2016
@Strate
Copy link
Author

Strate commented Dec 31, 2016

@akarzazi so, for example you want to return same value from the function, then you should use generic:

declare function handle<T extends Action1|Action2>(action: T): T

@akarzazi
Copy link

@Strate Do you have a type that extends Action1|Action2 ? (I guess No)
You do not need generics to return the same value
function handleAction(action: AnyAction) : AnyAction

@OliverJAsh
Copy link
Contributor

I have a problem which I think is related to this.

{
    type FooOrBar = "foo" | "bar"

    let x: FooOrBar
    (() => { // Error: Not all code paths return a value.
        if (x === 'foo') {
            x
            return 1
        } else if (x === 'bar') {
            x
            return 2;
        }
    })
}

TS should be smart enough to know I have exhausted the union here.

Is this related?

@mhegazy mhegazy added Suggestion An idea for TypeScript In Discussion Not yet reached consensus and removed Bug A bug in TypeScript labels Jul 23, 2018
@Adjective-Object
Copy link

Adjective-Object commented May 30, 2019

You do not need generics to return the same value
function handleAction(action: AnyAction) : AnyAction

I think the intent is for the following snippet:

type TypeA = {discriminator: 'a', count: number}
type TypeB = {discriminator: 'b', count: number}
function increment(x: TypeA | TypeB ): TypeA | TypeB {
  return {...x, count:x.count+1};
}

// this fails to typecheck, because `increment`
// does not return the exact type that it is being passed
type a: TypeA = increment({discriminator: 'a', count: 0});

Being able to constrain increment such that it returns the exact type it is passed.

@arxanas
Copy link

arxanas commented Sep 16, 2019

I believe this is the same issue:

type Foo = "bar" | "baz";

function okay(foo: Foo): number {
    switch (foo) {
        case "bar":
            return 1;
        case "baz":
            return 2;
    }
}

// Function lacks ending return statement and return type does not include 'undefined'.
function illegal<T extends Foo>(foo: T): number {
    switch (foo) {
        case "bar":
            return 1;
        case "baz":
            return 2;
    }
}

This is useful in practice. My actual use-case involves a type projection like function foo<T extends Id>(id: T, assocatedData: AssociatedData[T]>), like this:

const data = {
  "foo": {},
  "bar": {},
};

type AssociatedData = {
    "foo": {
        name: string,
    },
    "bar": {
        id: number,
    },
};

type Id = keyof typeof data;

function fancy<T extends Id>(id: T, data: AssociatedData[T]): number {
    // Not detected as exhaustive.
    switch (id) {
        case "foo":
            return 1;
        case "bar":
            return 2;
    }
}

(NB: the type projection does not refine types exactly how I would like here, but that's a separate issue.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
In Discussion Not yet reached consensus Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

7 participants