I've learned some things to share. It's not a definitive answer but worth noting:
When is a type parameter widened, from "a" to string?
https://github.com/microsoft/TypeScript/pull/10676
During type argument inference for a call expression the type inferred for a type parameter T is widened to its widened literal type if:
- all inferences for T were made to top-level occurrences of T within the particular parameter type, and
- T has no constraint or its constraint does not include primitive or literal types, and
- T was fixed during inference or T does not occur at top-level in the return type.
Type parameter T is widened if:
T has no constraint
- or its constraint does not include primitive or literal types
Widened
<T>(arr:T[]) has no constraint, so the types will be widened.
'a' = string
The unconstrained version is so generic, TS doesn't make too many assumptions about how to treat literals.
Not Widened
<T extends string>(arr:T[]) has a primitive constraint and isn't widened.
Since the constrained version is less generic, it preserves them. See PR for generally wanting to preserve literals as much as possible.
This is also an interesting tidbit:
In cases where literal types are preserved in inferences for a type parameter (i.e. when no widening takes place), if all inferences are literal types or literal union types with the same base primitive type, the resulting inferred type is a union of the inferences. Otherwise, the inferred type is the common supertype of the inferences, and an error occurs if there is no such common supertype.
All inferences of T are literal types ("a", "b") of the same base primitive type (string).
const fn = <T extends string>(arr:T[]) => arr
fn(['a', 'b']) // ('a' | 'b')[]
If the inference wasn't a literal type, the inferred type is the common supertype, string.
const fn = <T extends string>(arr:T[]) => arr
fn(['a' as string, "b"]) // widened to common supertype `string`
Generally speaking, TS is going to infer the "best" type for a given input. This function so happens to pass string literals (as an immutable expression) to identity, whose return type is T[] whose T is constrained by string. If it can preserve literals it will.
You can see there is an ocean of scenarios (pasted from that PR) where inference can preserve literals, and where it can't.
I don't have an answer about which combination of inference rules specifically enable this behavior, but generally, typescript is gonna typescript, and generally try to produce the output we want.