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

Literal string types downgraded to string at generic function call (which doesn't happen with a mixed type!) #40377

Closed
papb opened this issue Sep 4, 2020 · 9 comments
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug

Comments

@papb
Copy link

papb commented Sep 4, 2020

TypeScript Version: 4.0.2

Search Terms: literal string type, mixed union type, generic function call, type inference

Code

declare function mixed(): false | 'hello' | 'world';
declare function onlyStrings(): 'hello' | 'world';
declare function compare<T>(a: T, b: T): boolean;
compare(mixed(), ''); // Argument of type '""' is not assignable to parameter of type 'false | "hello" | "world"'.
compare(onlyStrings(), ''); // No error

Expected behavior:

  • Should error on last call: Argument of type '""' is not assignable to parameter of type '"hello" | "world"'.
  • Should provide autocomplete to 'hello' | 'world' when pressing Ctrl+Space at the empty string there.

Actual behavior: No error and no autocomplete.

Playground Link

Related Issues: #38968 (cc @mmkal)

@robbiespeed
Copy link

declare function compare<T, U>(a: T, b: T & U): boolean;

Should get the behaviour you're looking for from compare.
Something does seem weird with the generic parameter inference though, it becomes very evident here:

declare const words: 'hello' | 'world';
declare function compareStrings<T extends string>(a: T, b: T): boolean;
compareStrings(words, ''); // No error and T is inferred as "" | "hello" | "world"

Same also happens if you use '' as const in your original example.

Update
After more investigation, this seems to be expected behaviour, when multiple types are used to infer a single parameter if those types are of the same "base type" (object, string, number, boolean), the inferred parameter type will either be a union of all types, or it will infer the base type. Weather it infers the base type or a union depends on if the types used for inference are literal or not.

@papb
Copy link
Author

papb commented Sep 5, 2020

declare function compare<T, U>(a: T, b: T & U): boolean;

Should get the behaviour you're looking for from compare.

@robbiespeed Wow, it really works!! Wonderful! May I ask why it works? And what was your thought process to arrive at this solution?

After more investigation, this seems to be expected behaviour

Hmm, how do you know it is expected?

@robbiespeed
Copy link

May I ask why it works? And what was your thought process to arrive at this solution?

By using two generic parameters, you can guarantee that the type of a will be what's used to infer T, when T is used by itself for two params it's hard to tell whether T should be inferred from a or b. b: T & U is used to limit the type of b to be some extension of T. Using extends would also work, and I find it a bit cleaner than using T & U:

declare function compare<T, U extends T>(a: T, b: U): boolean;

Hmm, how do you know it is expected?

While I am not 100% certain, it does make sense when you look at in the sense that inference is a method of finding a common type for T across all areas it's used.

T becoming a union or base type simplification of the types for a and b also happens for the following cases:

declare function compare<T>(a: T, b: T): boolean;
compare({ foo: true }, { bar: true }); // T is { foo: boolean; bar?: undefined; } | { bar: boolean; foo?: undefined; }
compare(1, 2); // T is number
compare(1 as const, 2 as const); // T is 1 | 2
compare('a', 'b'); // T is string
compare('a' as const, 'b' as const);  // T is 'a' | 'b'

This all work the same down to version 3.3, with the exception of the as const examples since that feature wasn't implemented in older versions.

@papb
Copy link
Author

papb commented Sep 5, 2020

@robbiespeed I see! Nice. Very clever!! Thank you!!

[...] it does make sense when you look at in the sense that inference is a method of finding a common type for T across all areas it's used.

Thanks, everything you said makes sense, especially the part where you compare using and not using as const. However, why is it that as I mentioned in my first post, this doesn't happen with false | 'hello' | 'world'? Why, when I mix string literal types with a boolean type the behavior changes dramatically like this? Shouldn't T be inferred as false | string or perhaps boolean | string then? I find it strange that although 'hello' | 'world' is more specific than false | 'hello' | 'world', the inference happens to be much smarter with the less specific type...

@robbiespeed
Copy link

Why, when I mix string literal types with a boolean type the behavior changes dramatically like this?

That part seems a bit strange to me as well. I would suggest leaving this issue open, and hopefully someone on the Typescript team has an answer.

@RyanCavanaugh RyanCavanaugh added the Working as Intended The behavior described is the intended behavior; this is not a bug label Sep 9, 2020
@RyanCavanaugh
Copy link
Member

The desired behavior here is very much context-dependent, and as such there are special rules around unions of literals of a common base type.

@robbiespeed
Copy link

@RyanCavanaugh is there anywhere in the handbook that describes the rules? Would be nice to have somewhere to point to that avoids confusion on what is intended behaviour.

@typescript-bot
Copy link
Collaborator

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

@papb
Copy link
Author

papb commented Sep 12, 2020

@RyanCavanaugh I would like to know these special rules better too :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug
Projects
None yet
Development

No branches or pull requests

4 participants