Typescript: default value of generic argument is not assignable to the extended type

Created on 26 Oct 2018  ·  11Comments  ·  Source: microsoft/TypeScript

TypeScript Version: 3.2.0-dev.20181026

Search Terms:
default argument
default parameters
default value
default generic value
not assignable to
Code

type Z = "a" | "b"

// works fine
const x: Z = 'a'

function y<T extends Z>(a: T) { }

// works fine
y('a')  

// When you add default value the autocomplete is working fine, suggesting 'a' and 'b' but type error is present.
// Error: Type '"a"' is not assignable to type 'T'.
function y1<T extends Z>(a: T = 'a') { }

// This also working
function y2(a: "a" | "b" = 'a') { }

Expected behavior:
'a' should be assignable to T
Actual behavior:
Type Error

Playground Link:
http://www.typescriptlang.org/play/#src=type%20Z%20%3D%20%22a%22%20%7C%20%22b%22%0D%0A%0D%0Aconst%20x%3A%20Z%20%3D%20'a'%0D%0A%0D%0Afunction%20y%3CT%20extends%20Z%3E(a%3A%20T)%20%7B%20%7D%0D%0A%0D%0Ay('a')%20%20%0D%0A%0D%0Afunction%20y1%3CT%20extends%20Z%3E(a%3A%20T%20%3D%20'a')%20%7B%20%7D%0D%0A

Related Issues:
Nope.

Question

Most helpful comment

I tried coming up with a workaround for @DeTeam's example, but then I hit #29400.

- function f<T extends boolean>(some: T = false) {}
+ function f<T extends boolean>(some: T | false = false) {}

All 11 comments

I think this is related, but not identical to, this issue here #21521.

I would wait on the opinion from someone in the team...but my initial reading of the problem is that the choice to use default arguments is purely syntactic to keep things nice.

Just from the syntax y1() we know to drop in the default argument, and this is completely independent from the instantiation of the type parameters. However you can then run into unsound things like:

y1<"b">(); // argument will be 'a', says it has type 'b'.

To make your suggestion sound would require checking the default argument per instantiation, which is extra complexity that the TS team might not be in favour of. Instead, the current solution is to just pick a default argument that will always be consistent with the type parameter, even if this is limiting.

function y1<T extends Z>(a: T = 'a') { }

is correctly an error - you can write

y1<"a" & { __brand: void }>();

to which "a" would not be assignable (it'd be missing the __brand property). Sadly, extends of a union is not quite a "one of" constraint.

EDIT: apologies, I checked this with strictNullChecks turned off

it's a wider problem not only for unions. in all cases these examples should report error on call site

/* The a argument is optional*/
function y2<T extends { name: string }>(a?: T) { return a.name }

y2()
y2<{ name: string, age: number }>()

I wanted to get something similar to the code below running.
Faced the same question.

function f<T extends boolean>(some: T = false) {}

@weswigham @jack-williams I understand that covering these cases positively might result in additional complexity, so the question is — does it make sense to include/implement at some point in the future or maybe close this ticket and say "not planning to support"? Just for clarity 😄

false isn't assignable to T - T can be jnstantiated as either true or a subtype of false - so the assignment is not allowed. You must produce a T to assign to a T.

@weswigham does it mean that it's practically impossible to just provide a value here, the only way would be using another generic function which can produce T?

UPD: after some thinking, seem my question does not make sense — this is impossible in the situation above to create value of T from thin air.

I tried coming up with a workaround for @DeTeam's example, but then I hit #29400.

- function f<T extends boolean>(some: T = false) {}
+ function f<T extends boolean>(some: T | false = false) {}

false isn't assignable to T - T can be jnstantiated as either true or a subtype of false - so the assignment is not allowed. You must produce a T to assign to a T.

@weswigham Is there a compelling use case for allowing a subtype of false to be possible?

As of now, I'd rather see the following be supported:

function f<T extends boolean>(a: T = false, b: T extends true ? 1 : 0) {}

But I might just be in the weeds here. 🤷‍♂️

edit: Actually, the false as T workaround is simple enough. ;)

@aleclarson

Is there a compelling use case for allowing a subtype of false to be possible?

Yes, in the future you might have name subtyping similar to flow.

In Flow:

opaque type invalid: false = false;
// invalid is assignable to false, but false is not assignable to invalid

This issue has been marked as 'Question' and has seen no recent activity. It has been automatically closed for house-keeping purposes. If you're still waiting on a response, questions are usually better suited to stackoverflow.

I just found a way to avoid this issue using function overloads: https://twitter.com/kripod97/status/1248551695065456641

JavaScript vs TypeScript implementation of the same function. The latter infers its return type when a second parameter is given.

Applying keyof on the resulting object returns the union of concrete numeric values in steps. This makes code autocomplete work wonderfully.

Please refer to the implementation above and usage for further details.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

CyrusNajmabadi picture CyrusNajmabadi  ·  3Comments

MartynasZilinskas picture MartynasZilinskas  ·  3Comments

weswigham picture weswigham  ·  3Comments

Roam-Cooper picture Roam-Cooper  ·  3Comments

dlaberge picture dlaberge  ·  3Comments