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 inference when using extend in a generic is not consistent #59993

Open
ShacharHarshuv opened this issue Sep 17, 2024 · 2 comments
Open

Type inference when using extend in a generic is not consistent #59993

ShacharHarshuv opened this issue Sep 17, 2024 · 2 comments
Labels
Question An issue which isn't directly actionable in code

Comments

@ShacharHarshuv
Copy link

πŸ”Ž Search Terms

type inference when extend condition for a generic as const narrow inconsistent

πŸ•— Version & Regression Information

  • This is the behavior in every version I tried

⏯ Playground Link

https://www.typescriptlang.org/play/?#code/CYUwxgNghgTiAEAzArgOzAFwJYHtVIEYAeAFQD4AKADwC54SBKOkgbgCgA3WeKA+AXkIUA5AAkQECDmEMW8APTz4AZwwwsqAOZs2oSLAQp02PEgBMpeCCoYQqYMpVqNmyrXpN67LjB5mB5iLiktKyCkoARMFSEfBEALTx8ADuABYAniqpOMgQwPAYqQjWtvbwYKlQWgiFCMlQmRjpAA4IWI4aiCAwcMAA-PCiOMnwwDjwAJLwyMo1ReV4wFgm+ABGyBjwALZQANZtm53dvTyOqupafTp60HBIaJi4+IgAzJYldg5OF5rwAD7wVDILarbpuZieVicbhQF4BV5BCRSGRyRTwKJInCxBJJQrteD42oqKBbBBQRxEqCrHAcEAAGng602ymyIyg01mYHJdQykxSOTyKSqmzxjlBlQ4uBg13At0MDxWSAALO8bJ8zs4tABtAC64I8zG8MKV8KVFC1Ykxwh1YTR5xcuriiRSvOUyGAoFQGAgmWWBMcqBwh1QXR6IHy5IWqFUV10soM92MTyQAFZVaUvtScBAQFVdfrGIbob4oCn4SnzYgoBBZjbUUoqzWQI60TiXZlI2AcMovUgcL4szmqmKNoCg33fPatB18KKOQgubM+kA

πŸ’» Code

declare function f1<T>(x: T): T;
var a1 = f1('Hello'); // string

declare function f2<T extends string>(x: T): T;
var a2 = f2('Hello'); // "Hello" <-- why should the extend change the way type is inferred? How do I use the condition but make it inferred as string?

declare function f3<T extends string | number>(x: T): T;
var a3 = f3('Hello'); // "Hello" <-- this is the same as the above, but show a usecase why I would want this behavior

declare function f4<T extends string[]>(x: T): T;
var a4 = f4(['Hello']); // string[] <-- why suddently it is not inferred as const?

declare function f5<T extends boolean[]>(x: T): T;
var a5 = f5([false]); // false[] // <-- why as cosnt for booleans but not for strings in this use case?

πŸ™ Actual behavior

The type inference is not consistent in the above cases, and it's unclear to me (a) what is the logic behind this, and (b) how do I accomplish a different behavior if I need it. More specifically:

  • Why adding extends string means that the type is now inferred as const?
  • Why using an array changes the behavior back to not using as const?
  • Why extends string[] and extends boolean[] behaves so differently? Why booleans gets the const treatment while strings don't?

πŸ™‚ Expected behavior

Since there is a dedicated feature when we want as const for generics, I think that it should be inferred not as const in all of these use cases. This offers the maximum flexibility for consumers as they can pick the behavior they want.

I would also be happy for any work around for this. Let's take this example:

function f<T extends string | number>(value: T): T;

How do I make it so that:

  1. f('somestring') is inferred as string
  2. f<'blue' | 'red'>('red') is inferred as `red'

Also - how do I solve the issue with arrays? Specifically, if I have the function:

function f<T extends any[]>(value: T): T;

Can I make it so that f(['string', true]) will be inferred as [string, boolean] instead of [string, true]?

Additional information about the issue

No response

@RyanCavanaugh
Copy link
Member

I'm confused because you know about const but aren't using it where doing so would give you the result you want.

Inference is presented with a set of candidates plus a constraint and tries to figure out what you wanted. This talk goes into some examples - it doesn't talk about literals but you can see from that talk why inference makes some decisions but not others.

Why adding extends string means that the type is now inferred as const?

Because if you wanted the function to return string when passed "hello", you wouldn't have written the constraint in the first place.

Why using an array changes the behavior back to not using as const?

Because the prior argument no longer applies (e.g. you might have a subtype of array that you're specializing to)

Why booleans gets the const treatment while strings don't?

Booleans are a union but strings aren't

@RyanCavanaugh RyanCavanaugh added the Question An issue which isn't directly actionable in code label Sep 18, 2024
@ShacharHarshuv
Copy link
Author

I'm confused because you know about const but aren't using it where doing so would give you the result you want.

I'm not sure how I'm supposed to get the result I want using const, as it seems to be the opposite of what I want.

Imagine this example: function f<T extends number | string>(value: T): T. How do I make it infer f('hello') as string, f(1) as number, and f<'red' | 'blue'>('red') as 'red' | 'blue' ?
In that case, there is a reason to the constraint - I do want to narrow number | string but I want to narrow it to either number or string.

The only solution I was able to figure out is using overloads instead:

function f(value: string): string;
function f(value: number): number;
function f(value: string | number): string | number;

However, this doesn't work for more complicated examples. Consider this:

function f<T extends (object | boolean)[]>(value: T): T;

I would expect f([{name: 'hello', true}]) to be inferred as ({name: 'hello'}|boolean)[] but it is inferred as ({name: 'hello'}|true)[].

Is it possible to type in a way that this would work?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Question An issue which isn't directly actionable in code
Projects
None yet
Development

No branches or pull requests

2 participants