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

Conditional type narrowing for function arguments doesn't work #29947

Closed
vshab opened this issue Feb 17, 2019 · 4 comments
Closed

Conditional type narrowing for function arguments doesn't work #29947

vshab opened this issue Feb 17, 2019 · 4 comments
Labels
Duplicate An existing issue was already created

Comments

@vshab
Copy link

vshab commented Feb 17, 2019

TypeScript Version: 3.4.0-dev.201xxxxx

Search Terms:
conditional type narrowing function arguments

Code

type IfNull<T, U> = T extends null ? U : undefined;

interface Value {
  do: () => void;
}

// foo type shows as "function foo(error: Error | null, value: Value | undefined): void"
function foo(error: Error | null, value: IfNull<typeof error, Value>) {
  if (error) {
    return;
  }
  
  // error TS2532: Object is possibly 'undefined'.
  value.do();
}

// Shouldn't be allowed:
foo(null, undefined);
foo(new Error(), { do: () => {} });

// However these work as expected:
let error: Error | null = null,
  value: IfNull<typeof error, Value> = { do: () => {} };

value.do();

strictNullChecks is true.

Expected behavior:
Typescript should narrow types for function arguments as it does for variable declaration.

Actual behavior:
Function arguments are treated without narrowing.

Playground Link:
https://www.typescriptlang.org/play/#src=type%20IfNull%3CT%2C%20U%3E%20%3D%20T%20extends%20null%20%3F%20U%20%3A%20undefined%3B%0D%0A%0D%0Ainterface%20Value%20%7B%0D%0A%20%20do%3A%20()%20%3D%3E%20void%3B%0D%0A%7D%0D%0A%0D%0A%2F%2F%20foo%20type%20shows%20as%20%22function%20foo(error%3A%20Error%20%7C%20null%2C%20value%3A%20Value%20%7C%20undefined)%3A%20void%22%0D%0Afunction%20foo(error%3A%20Error%20%7C%20null%2C%20value%3A%20IfNull%3Ctypeof%20error%2C%20Value%3E)%20%7B%0D%0A%20%20if%20(error)%20%7B%0D%0A%20%20%20%20return%3B%0D%0A%20%20%7D%0D%0A%20%20%0D%0A%20%20%2F%2F%20error%20TS2532%3A%20Object%20is%20possibly%20'undefined'.%0D%0A%20%20value.do()%3B%0D%0A%7D%0D%0A%0D%0A%2F%2F%20Shouldn't%20be%20allowed%3A%0D%0Afoo(null%2C%20undefined)%3B%0D%0Afoo(new%20Error()%2C%20%7B%20do%3A%20()%20%3D%3E%20%7B%7D%20%7D)%3B%0D%0A%0D%0A%2F%2F%20However%20these%20work%20as%20expected%3A%0D%0Alet%20error%3A%20Error%20%7C%20null%20%3D%20null%2C%0D%0A%20%20value%3A%20IfNull%3Ctypeof%20error%2C%20Value%3E%20%3D%20%7B%20do%3A%20()%20%3D%3E%20%7B%7D%20%7D%3B%0D%0A%0D%0Avalue.do()%3B

Related Issues:
Didn't find.

@jack-williams
Copy link
Collaborator

A typeof type used in a signature will simply pick out the declared type of the identifier; there is no fundamental link back to the argument that will evolve over time. As you identify: if you look at the type of foo presented by the checker you see that any notion of error has been erased.

function foo(error: Error | null, value: Value | undefined): void {  }

One adjustment that will correctly flag the calls of foo that you would like to error is:

function foo<T extends Error | null>(error: Error | T, value: IfNull<T, Value>) {  }

To narrow value based on error is non-trivial, essentially it is dependent typing.

If you really want narrowing in the function you might have to do something like this:

function foo(...args: [Error, null] | [undefined, Value]) {
  if (args[0]) {
    return;
  }
  args[1].do();
}

@vshab
Copy link
Author

vshab commented Feb 18, 2019

@jack-williams Thank you for clarification.
But isn't it wrong? I mean why is the function signature being changed and as you said error has been erased?

One adjustment that will correctly flag the calls of foo that you would like to error is:

Interesting... Thanks! But I'm really more interested in narrowing inside of the function.

If you really want narrowing in the function you might have to do something like this:

Great! However when I'm trying to make it more user-friendly i.e.:

function foo(...[error, value]: [Error, null] | [undefined, Value]) {
  if (error) {
    return;
  }

  value.do();
}

It doesn't work. Am I missing something?

@jack-williams
Copy link
Collaborator

jack-williams commented Feb 18, 2019

Type narrowing does not work across variables. This is a design limitation that is tracked here #12184.

@RyanCavanaugh RyanCavanaugh added the Duplicate An existing issue was already created label Feb 19, 2019
@typescript-bot
Copy link
Collaborator

This issue has been marked as a 'Duplicate' and has seen no recent activity. It has been automatically closed for house-keeping purposes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Duplicate An existing issue was already created
Projects
None yet
Development

No branches or pull requests

4 participants