Reputation: 119
I want to narrow the type of a function argument based on a sibling property. I know that existential types would help but they are not available so I'm using a helper function. I currently have this thanks to jcalz in the comments
// helper function
function helper<T extends readonly unknown[]>(o: {
choices: T;
func: (opts: T[number]) => T[number];
}) {
return o;
}
helper({
choices: [
"one",
"two",
"three",
], // as const
func(o) { // should be "one" | "two" | "three", but is string
return o;
}
})
I want o
in func
to be of type "one" | "two" | "three"
, so my issue is that typescript doesn't infer the choices as a tuple but as string[]
. I can't add as const
to choices
because the function will be called in javascript.
I know that I could split the choices and the function into separate arguments to the helper function, but this is not possible since there will be more properties in the object. The purpose of the properties are to describe func
, what it needs as input and what it returns.
The final goal is to have the helper function written in typescript, but used as a type annotation in javascript. Therefore I can't provide any types when calling the function or use as const
where it is marked with a comment.
Upvotes: 2
Views: 179
Reputation: 328503
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.
Upvotes: 2