Reputation: 58
I'm trying to conditionally set the return type of a function, based on a static value in the same type.
Given the following code, is it possible to have type checking work as I've specified in the comments?
IE, if const taskX: Task
has the id: 'get-name'
, how can I make it so taskX.run()
should return according to the type defined as Outputs['get-name']
type Outputs = {
"get-name": {
name: string;
};
"get-age": {
age: number;
};
"get-favourite-fruits": ("apple" | "strawberry" | "melon")[];
};
type Task = {
// This should always be a static property defined in `Outputs`
id: keyof Outputs;
// How can I make this conditional and return the type from `Outputs` based on `id`
run: () => never;
};
const taskOne: Task = {
id: "get-name",
// This should be valid
run: () => {
return { name: "frankenstein" };
},
};
const taskTwo: Task = {
id: "get-age",
// This should also be valid
run: () => {
return { age: 22 };
},
};
const taskThree: Task = {
id: "get-favourite-fruits",
// This should fail with
// Type '("apple" | "banana")[]' is not assignable to type
// '("apple" | "strawberry" | "melon")[]'
run: () => {
return ["apple", "banana"];
},
};
Upvotes: 0
Views: 107
Reputation: 330171
You want Task
to be a discriminated union of the three different possible pairings of id
and run
. You can generate this programmatically from Outputs
by mapping each property of Outputs
to a value corresponding to this pairing, and then looking up all of the mapped properties to get the desired union:
type Task = {
[K in keyof Outputs]: { id: K, run: () => Outputs[K] }
}[keyof Outputs];
/* produces
type Task = {
id: "get-name";
run: () => {
name: string;
};
} | {
id: "get-age";
run: () => {
age: number;
};
} | {
id: "get-favourite-fruits";
run: () => ("apple" | "strawberry" | "melon")[];
}*/
Then the rest of your code works as desired. Valid tasks are accepted:
const taskOne: Task = {
id: "get-name",
run: () => {
return { name: "frankenstein" };
},
}; // okay
const taskTwo: Task = {
id: "get-age",
run: () => {
return { age: 22 };
},
}; // okay
And invalid tasks are rejected:
const taskThree: Task = {
id: "get-favourite-fruits",
run: () => { // error!
// Type '() => ("apple" | "banana")[]' is not assignable to type
// '(() => { name: string; }) | (() => { age: number; }) |
// (() => ("apple" | "strawberry" | "melon")[])
return ["apple", "banana"];
},
};
const taskFour: Task = { // error!
// ~~~~~~~~
// Type '{ id: "get-favourite-fruits"; run: () => { age: number; }; }'
// is not assignable to type 'Task'.
id: "get-favourite-fruits",
run: () => {
return { age: 123 };
},
};
The errors for invalid tasks might be a bit different from the particular one you were expecting. For taskThree
the compiler complains that run()
's return is incompatible with every possible Task
member; "not only isn't it the right array, but it isn't even one of the right types for get-name
or get-age
!" For taskFour
the compiler sees that run()
returns an {age: number}
but the id
is get-favorite-fruits
and complains that the whole value is wrong without highlighting any particular property.
Upvotes: 2
Reputation: 1232
You can achieve that by declaring a type which is either of them and based on the id the compiler can tell which one of those it is.
type GetNameTask = {
id: "get-name";
run: () => {name: string};
}
type GetAgeTask = {
id: "get-age";
run: () => {number: string};
}
type GetFavoriteFruitsTask = {
id: "get-favourite-fruits"
run: () => ("apple" | "strawberry" | "melon")[];
}
type Task = GetNameTask | GetAgeTask | GetFavoriteFruitsTask
For more a clean systax you can also try this:
type GenericTask<TId, TOutput> = {
id: TId;
run: () => TOutput
}
type GetNameTask2 = GenericTask<"get-name", {name: string}>
type GetAgeTask2 = GenericTask<"get-age", {age: number}>
type GetFavoriteFruitsTask2 = GenericTask<"get-favourite-fruits", ("apple" | "strawberry" | "melon")[]>
type Task = GetNameTask2 | GetAgeTask2 | GetFavoriteFruitsTask2
Upvotes: 0