You'd like helper() to treat the array literal value ["one", "two", "three"] as having the type ["one", "two", "three"]; that is, as a tuple of string literal types instead of as just string[]. This is easy enough to do from the caller's side of the function; callers can use a const assertion like ["one", "two", "three"] as const. But in cases where the callers don't want to (or cannot) use a const assertion, it would be nice to get the function itself to do it.
I opened microsoft/TypeScript#30680 a while ago asking for some easy way to do this. Maybe it would look like:
// INVALID TYPESCRIPT SYNTAX, DO NOT TRY THIS
// vvvvv
function helper<T extends const readonly unknown[]>(o: {
choices: T;
func: (opts: T[number]) => T[number];
}) {
return o;
}
where const could somehow be applied to the generic type parameter. Unfortunately, this is not currently part of the language.
Luckily, there are tricks to get the compiler to behave somewhat similarly.
If you want to give the compiler a hint that it should infer literal types, you can look at microsoft/TypeScript#10676. If a type parameter is constrained to a type that includes string, then the compiler will tend to infer string literal types. The same goes for number. So while an unconstrained T (like T extends unknown) will infer string when it sees "one", a constrained T extends string will infer "one". If you don't really want to constrain the type parameter, then you need to come up with the widest possible constraint that contains string and/or number explicitly. You can't use the unknown type since T extends string | unknown is immediately collapsed to T extends unknown. You can however do something like
type Narrowable = string | number | bigint | boolean |
symbol | object | undefined | void | null | {};
And then write T extends Narrowable instead of T extends unknown. Or in your case, T extends readonly Narrowable[] instead of T extends readonly unknown[].
If you want the compiler to infer tuples instead of array types, there are other tricks. TypeScript 4.0 introduced variadic tuple types. If U is an arraylike type parameter, then the compiler will tend to see (arr: U) and infer an unordered array type for U. But if you write (arr: [...U]) or `(arr: readonly [...U]), it will tend to infer a tuple type.
Combining these gives:
function helper<T extends readonly Narrowable[]>(o: {
choices: readonly [...T];
func: (opts: T[number]) => T[number];
}) {
return o;
}
And you can verify that it works:
helper({
choices: [
"one",
"two",
"three",
], // as const
func(o) { // "one" | "two" | "three"
return o;
}
})
Looks good.
Playground link to code