Through my own exploration, I've come across some interesting insights worth sharing. While not a definitive answer, they are certainly noteworthy:
Under what circumstances does a type parameter shift from being "a"
to string
?
Find out more on this here
When it comes to inferring types for call expressions and widening type parameters, the process occurs as follows:
- If all inferences for type parameter T refer to top-level occurrences of T within the specific parameter type,
- If T has no constraint or its constraints do not include primitive or literal types,
- If T was fixed during inference or does not appear at the top level in the return type.
The widening of the type parameter T
happens if:
T
is unconstrained,
- or its constraint excludes primitive or literal types.
Widening Process
Using <T>(arr:T[])
with an unconstrained type parameter results in widened types.
For instance, 'a'
equals string
.
In the case of the unconstrained version, TypeScript refrains from making too many assumptions about how to handle literals.
No Widening Process
Utilizing
<T extends string>(arr:T[])
, where there is a primitive constraint, prevents widening from occurring.
Since the constrained variant is less generic, it maintains the original input. Refer to the PR for a desire to retain literals whenever possible.
An intriguing observation is also provided:
Literal types are maintained in inferences for a type parameter when no widening transpires. If all inferences consist of literal types or literal union types stemming from the same base primitive type, the resulting inferred type becomes a union of these inferences. Otherwise, the inferred type turns into the common supertype of the inferences, leading to an error if no such shared supertype exists.
All inferences of type parameter T constitute literal types ("a"
, "b"
) derived from the base primitive type - string
.
const fn = <T extends string>(arr:T[]) => arr
fn(['a', 'b']) // ('a' | 'b')[]
If the inference does not involve a literal type, the common supertype, string
, emerges.
const fn = <T extends string>(arr:T[]) => arr
fn(['a' as string, "b"]) // undergoes widening to the common supertype `string`
As a general rule, TypeScript strives to deduce the most suitable type for a given input. In the example illustrated, immutable string literals are passed to identity
, whose return type is T[]
. The type T
adheres to the constraint of string
, hence, preserving literals whenever feasible.
This detailed analysis showcases diverse scenarios (referenced from the mentioned PR) where inference can either maintain or alter literals.
While I cannot pinpoint the exact combination of rules that enable this behavior, it's safe to assume that TypeScript will operate according to its inherent nature and strive to deliver the desired output.