I am stuck in trying to figure out how to type a return type for a function, which would return an object using the keys of generic type from one of the function arguments.
type Input<A> = { [key: string]: A };
type Output<T> = { [key in keyof T]: null };
const track = <T extends string>(input: Input<T>): Output<typeof input> => {
return Object.entries(input).reduce((previous, [key, val]) => ({
...previous,
[key]: null,
}), {} as Output<typeof input>);
}
type Test = 'union' | 'string';
const { one } = track<Test>({ one: 'union', two: 'string' });
I would like to be able to have autocomplete on const { one }
to be able to see all of the keys that the input
argument has, but it's not working. Another requirement would be to be able to specify the allowed values the Input<A>
type can have.
I got it to work when I let typescript infer the T extends string
type, but in my case it cannot be inferred and I would like to be able to pass it.
What am I doing wrong?
If I use : Output<Input<T>>
then it also doesn't work, and I'm assuming that is because typescript is considering the Input<T>
there to be something entirely different than the input
argument with the same type (rightfully so).
Could anyone please shed some light on this?
Here's a playground
CodePudding user response:
If you want to capture the key names of the arugment object in the return type, then that whole object needs to be part of the generic parameter.
type Input<A> = { [key: string]: A };
type Output<T> = { [key in keyof T]: null };
const track = <T extends Input<string>>(input: T): Output<T> => {
return Object.entries(input).reduce((previous, [key, val]) => ({
...previous,
[key]: null,
}), {} as Output<T>);
}
Here, instead of capturing just the parameter from Input<T>
you capture a whole input object as T
. This has the type information about the keys on it, so those can be used on the output type.
Then call it like:
const result = track({ one: 'union', two: 'string' });
result.one // type is `null`, and has autocomplete
"but doing it this way seems to lose the autocomplete inside the Input, where I would like to specify the available possible strings that can be used."
What I assume you mean is that this is allowed:
const result = track<'a' | 'b'>({ one: 'a', two: 'b' });
but this is an error:
const result = track<'a' | 'b'>({ one: 'a', two: 'c' });
// ^ value not allowed
So here's the problem with that. This will require two generic parameters, one for the list of allowed values, and one for the argument being passed in. And you want to explicitly set one, and infer the other.
Typescript does not currently allow you to do. All generic parameters must be inferred, or all must be explicit.
You could delcare both with something like:
const track = <
V extends string,
T extends Input<V>
>(input: T): Output<T> => {
//...
}
const result = track<
'aa' | 'ab',
{ one: 'aa', two: 'ab' }
>({ one: 'aa', two: 'ab'});
result.one // type is `null`, and has autocomplete
But's that's not very satisfying.
And you can't infer both because the list of allowed values isn;'t in the list of arguments.
const result = track({ one: 'aa', two: 'ab'});
// the allowed values `V` here is simply inferred as `string`,
// which isn't helpful.
The typical work around is to break it up into the multiple function calls like so:
const track = <V extends string>() => {
return <T extends Input<V>>(input: T): Output<T> => {
return Object.entries(input).reduce((previous, [key, val]) => ({
...previous,
[key]: null,
}), {} as Output<T>);
}
}
const result = track<'aa' | 'ab'>()({ one: 'aa', two: 'aa' });
// ^ note intermediate function call
result.one
Now the allowed values V
can be explicitly set, with a function call. This then returns a new function where the argument type can be inferred.
It's not the most pleasant work around, but until Typescript gives us partial inference of generic parameters it's the best we got.