Issue
I have a function that takes an argument and a processing function, and always returns a tuple. How can I tell typescript that if I pass one type of argument, the processing function can accept only that type, and if I pass the other type, the processing function will be able to accept that one instead? I have boiled my problem down to the below:
type one = number[]
type two = Record<string, number[]>
function proc_one(arg: one): [number] {
console.log(arg)
return [1]
}
function proc_two(arg: two): [number] {
console.log(arg)
return [2]
}
function myfun<T>(
arg: T extends one ? one : T extends two ? two : never,
process: T extends one ? typeof proc_one : T extends two ? typeof proc_two : never
) {
/*
Argument of type 'two | one' is not assignable to parameter of type 'one & two'.
Type 'two' is not assignable to type 'one & two'.
Type 'two' is missing the following properties from type 'number[]': length, pop, push, concat, and 26 more.
*/
return process(arg)
}
// should work
myfun([1], proc_one)
myfun({two: [2]}, proc_two)
// should error
myfun([1], proc_two)
myfun({two: [2]}, proc_one)
How can I write my type constraints to achieve this?
Solution
The TypeScript type checker can't really verify much about the behavior of conditional types that depend on generic type parameters. So even if you were positive that a type like T extends One ? typeof proc_one : T extends Two ? typeof proc_two : never would behave properly with respect to T, the compiler can't see it. Trying to call process(arg) when process is of a generic conditional type will almost certainly result in an error. In situations where such an approach is necessary, you need to use type assertions to suppress the error.
Luckily your code doesn't seem to need conditional types. Instead, you can constrain the T type parameter to the union type One | Two, and then have process be of type (arg: T) => [number] (or an equivalent type):
function myfun<T extends One | Two>(
arg: T,
process: (arg: T) => [number]
) {
return process(arg); // okay
}
myfun([1], proc_one); // okay
myfun({ two: [2] }, proc_two) // okay
myfun([1], proc_two) // error
myfun({ two: [2] }, proc_one) // error
Now everything behaves as expected. The compiler is happy to allow process(arg) because process() takes a single argument of type T, which is the type of arg.
That's basically it, but the phrasing of the question as accepting "exactly" one member of the union leads me to talk a little more. TypeScript doesn't really have a great way to say that a generic type parameter must be exactly one type; the constraint T extends One | Two allows T to be One or Two as desired, but it also allows it to be the full One | Two union. Usually this situation won't crop up, since your arg argument is unlikely to be of type One | Two. But it technically can happen:
myfun(Math.random() < 0.5 ? [1] : { two: [2] }, (x: One | Two) => [3]); // okay
That code is still safe here, but it might be unexpected. We'd like to say "T should be exactly one of One or Two", but there is no direct support for that. See microsoft/TypeScript#27808 for an open feature request for such functionality.
For now, I'd say leaving open the possibility that someone passes in One | Two isn't a big deal for the example here, and any effort to prevent it will just make your code more fragile. So that's a good place to leave it.
Answered By - jcalz
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.