Reputation: 18240
I have a function that receives an options
argument with a kind
attribute. The possible values for kind
is a small set of values. So its basically an enum.
depending on the kind
the function should have a different return value. While all possible return values extend from some common base type.
I can achieve what I want with overloads, but then the function itself is not typed very well:
function test(options: Scenarios['bar']['options']): Scenarios['bar']['ret'];
function test(options: Scenarios['foo']['options']): Scenarios['foo']['ret'];
function test(options: any): any {
...
};
Is there a nice way to type this using generics? It would be perfect it a if(options.kind === 'foo') { return ... }
would also correctly enforce the correct return type.
Thats what I tried but it isnt working.
type Base {
a: string;
}
type Foo {
b: string;
}
type Bar {
c: string;
}
interface Scenarios {
foo: { options: { kind: 'foo', input: string }, ret: Foo },
bar: { options: { kind: 'bar' }, ret: Bar },
}
function test<S extends keyof Scenarios, O extends Scenarios[S]['options'], R extends Scenarios[S]['ret']>(options: O): R {
const out: Partial<R> = {
a: 'one',
};
if(options.kind === 'foo') {
out.b = options.input;
}
if(options.kind === 'bar') {
out.c = "whatever"
}
return out;
}
Here neither O
nor R
seems not to be correctly typed. I get multiple errors:
a: 'one',
errors with Object literal may only specify known properties, and 'a' does not exist in type 'Partial'options.input
errors with Property 'input' does not exist on type 'O'.out.b
(and out.c
) errors with Property 'b' does not exist on type 'Partial'.Upvotes: 5
Views: 886
Reputation: 2534
I'm not really an expert on this subject, but after playing awhile with your request, I realize a few things:
These being said, here's a solution I came up with:
type Base = { // I'm assuming this is the base type for Foo and Bar
a: string;
}
interface Foo extends Base { // so I shall modify here a bit
b: string;
}
interface Bar extends Base { // and here of course
c: string;
}
interface Scenarios {
// Now I put the return type inside the option, to make the discrimination work
// Of course, I need to make 'ret' optional, or your input won't make sense
foo: { kind: 'foo', input: string, ret?: Foo },
bar: { kind: 'bar', ret?: Bar },
}
// Notice the use of 'Required' here; that brings the actual type of 'ret' back
function test<X extends keyof Scenarios>(options: Scenarios[X]): Required<Scenarios[X]>['ret'] {
let data = options as Scenarios[keyof Scenarios]; // create a union type
data.ret = { a: 'one' } as Scenarios[keyof Scenarios]['ret']; // type assertion
if (data.kind === 'foo') {
data.ret!.b = data.input; // finally the discrimination works!
}
if (data.kind === 'bar') {
data.ret!.c = "whatever"
}
return data.ret!;
}
Ok, so far so good, unfortunately I still can't make TypeScript to infer the generic argument automatically. Say if I run:
var result = test({ kind: 'foo', input: "aas" }); // oops
Then TypeScript still cannot figure out that result
is of type Foo
. But, of course, this kind of auto-inference is of very low practical value, since even if it works, it works only when you literally type the word 'foo' in the argument of your function, and if you can do that, what stops you from typing the generic argument?
You may try these code in this Playground Link
I just found the solution to my last problem:
function test<X extends keyof Scenarios>(options: { kind: X } & Scenarios[X]): Required<Scenarios[X]>['ret'] {
....
}
var result = test({ kind: 'foo', input: "aas" }); // It works!
The trick is to add { kind: X }
to the parameter declaration.
See this Playground Link.
Upvotes: 2