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:
Argument of type '""' is not assignable to parameter of type '"hello" | "world"'.'hello' | 'world' when pressing Ctrl+Space at the empty string there.Actual behavior: No error and no autocomplete.
Related Issues: #38968 (cc @mmkal)
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.
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?
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.
@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...
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.
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.
@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.
This issue has been marked 'Working as Intended' and has seen no recent activity. It has been automatically closed for house-keeping purposes.
@RyanCavanaugh I would like to know these special rules better too :)