Reputation: 2240
How can I properly infer the return type based on a function that has one parameter that is a union of two types?
I've tried the following with conditional types, but it does not work (see inline comment for the typescript error):
type Status = 'statusType'
type GoalActivity = 'goalActivityType'
type Argument = { type: 'status'; status: Status | null } | { type: 'goalActivity'; goalActivity: GoalActivity | null }
const handleReaction = (arg: Argument): Argument extends { type: "status" } ? Status : GoalActivity => {
if (arg.type === 'status') {
return 'statusType' // Type '"statusType"' is not assignable to type '"goalActivityType"'.
} else {
return 'goalActivityType'
}
}
I've also tried the following using a form of function overloading for arrow functions (as described here), but this also results in a TypeScript error and also uses "any" which loses most of the typing benefits inside the function definition:
type Status = 'statusType'
type GoalActivity = 'goalActivityType'
type HandleReaction = {
(arg: { type: 'status'; status: Status | null }): Status
(arg: { type: 'goalActivity'; goalActivity: GoalActivity | null }): GoalActivity
}
const handleReaction: HandleReaction = (arg: any) => { // Type '"goalActivityType"' is not assignable to type '"statusType"'.
if (arg.type === 'status') {
return 'statusType'
} else {
return 'goalActivityType'
}
}
This question is similar to this one, but with the difference being that the function parameter is an object.
Upvotes: 2
Views: 4208
Reputation: 965
Your argument objects should have their own types. Once they do, you can use function overloading like this:
type Status = { type: 'status', status: 'statusValue' };
type GoalActivity = { type: 'goalActivity', goalActivity: 'goalValue' };
function handleReaction(arg: Status): Status['status']
function handleReaction(arg: GoalActivity): GoalActivity['goalActivity']
function handleReaction(arg: Status | GoalActivity) {
if ('status' in arg) {
return arg.status;
} else {
return arg.goalActivity;
}
}
const a = handleReaction({ type: 'status', status: 'statusValue' })
// ^? 'statusValue'
const b = handleReaction({ type: 'goalActivity', goalActivity: 'goalValue' })
// ^? 'goalValue'
Upvotes: 0
Reputation: 17504
The first thing is you haven't used generic type for your argument that would result in typescript
will never infer the correct type based on your input (you can imagine generic type is parameter, tsc
requires it to calculate the result based on your input).
In short,
const handleReaction = (arg: Argument): Argument extends { type: "status" } ? Status : GoalActivity => { // ... }
will always return Status | GoalActivity
as return type.
Of course, you have to use generic type here as your argument. I'll split your code out with inline explanation:
type Status = 'statusType'
type GoalActivity = 'goalActivityType'
type StatusObj = { type: 'status'; status: Status | null };
type GoalActivityObj = { type: 'goalActivity'; goalActivity: GoalActivity | null }
type Argument = StatusObj | GoalActivityObj;
// Define returned type based on a input argument `T`
type ReturnType<T> = T extends StatusObj ? Status : GoalActivity;
// Generic type should be used here
const handleReaction = <T extends Argument>(arg: T): ReturnType<T> => {
if (arg.type === 'status') {
// Q: Why do we have to cast here?
// A: Any returned type can't assign to statement of `type ReturnType<T> ...`
// but luckily `tsc` still allows us to cast back since they are all string literal
return 'statusType' as ReturnType<T>;
} else {
return 'goalActivityType' as ReturnType<T>;
}
}
Upvotes: 4