In a simplified scenario I have the types:
type Obj = {
one: number;
two: number;
three?: number;
four?: number;
five?: number;
}
type ObjOptional = {
three?: boolean;
four?: boolean;
five?: boolean;
}
I'm using Axios to fetch remote data with something like the following:
function GetRemoteObj(id: number, whatToInclude: ObjOptional) {
return axios.get<Obj>(`api/obj/${id}`, { params : whatToInclude });
}
Does typescript allow to define GetRemoteObj
in a way that returned type is dependend to the parameter whatToInclude
?
Example 1:
const obj = (await GetRemoteObj(1, { three: false, four: true })).data;
here obj
type will be
{
one: number;
two: number;
four: number;
}
Example 2:
const obj = (await GetRemoteObj(1, { })).data;
here obj
type will be
{
one: number;
two: number;
}
Example 3:
const obj = (await GetRemoteObj(1, { three: true, four: true, five: true })).data;
here obj
type will be
{
one: number;
two: number;
three: number;
four: number;
five: number;
}
In a simplified scenario I have the types:
type Obj = {
one: number;
two: number;
three?: number;
four?: number;
five?: number;
}
type ObjOptional = {
three?: boolean;
four?: boolean;
five?: boolean;
}
I'm using Axios to fetch remote data with something like the following:
function GetRemoteObj(id: number, whatToInclude: ObjOptional) {
return axios.get<Obj>(`api/obj/${id}`, { params : whatToInclude });
}
Does typescript allow to define GetRemoteObj
in a way that returned type is dependend to the parameter whatToInclude
?
Example 1:
const obj = (await GetRemoteObj(1, { three: false, four: true })).data;
here obj
type will be
{
one: number;
two: number;
four: number;
}
Example 2:
const obj = (await GetRemoteObj(1, { })).data;
here obj
type will be
{
one: number;
two: number;
}
Example 3:
const obj = (await GetRemoteObj(1, { three: true, four: true, five: true })).data;
here obj
type will be
{
one: number;
two: number;
three: number;
four: number;
five: number;
}
Share
Improve this question
asked Jan 31 at 10:30
c.bearc.bear
1,44513 silver badges25 bronze badges
1
- 1 I think this is what you're looking for. – HairyHandKerchief23 Commented Jan 31 at 11:40
1 Answer
Reset to default 3Yes, you can define the function types to satisfy your use case, since the possibilities are more continuous than discrete; you could have many keys and they follow a similar pattern, then generics is a good approach.
If you had only a few options you could use function overloads which let you provide types for distinct function signatures.
To use generics, you will also need to formulate the type of the return value according to the generic, you can use mapped types for this.
I am guessing from your question that you want the function to always return an object that satisfies Obj
and also satisfies some type derived from ObjOptional
where keys with a true
value should be included with a number
value. This will be the core of the mapped type.
A simple first approach can be to say given a type that satisfies ObjOptional
, produce a type with the same keys and the value type number
if input value is true and never
if input key is false or undefined. So the generic bound is ObjOptional
. This look like:
type MapToReturn<T extends ObjOptional> = {
[K in keyof T]: T[K] extends true ? number : never;
};
{[K in keyof T]:..
takes each key from T as the keys for the resulting type, and provides you with K
to work with on the value type. T[K] extends true ? number : false
is a simple conditional type that returns number
if the value extends true
which in this case is true
since except any
only true
extends true
. And if not it returns never
.
We can test this with the inputs from your first example:
MapToReturn<{ three: false; four: true }>
The type we get is { three: never; four: number;}
This is a good start but not very helpful in practice since we cannot reasonably produce an object with key three
but value never
. However mapped types also allow you to remap keys using the as
keyword which allows us the move the conditional logic from the value onto the key. We can then write the type as:
type MapToReturn<T extends ObjOptional> = {
[K in keyof T as T[K] extends true ? K : never]: number;
};
In this case, if the input value at key K
is not true
the key will be dropped from the resulting type. And for the same input as before { three: false; four: true }
we get type { four: number; }
.
Next we should include Obj
in the response. A simple way of doing this is to produce an intersection &
with Obj
.
type MapToReturn<T extends ObjOptional> = Obj & {
[K in keyof T as T[K] extends true ? K : never]: number;
};
This works but since Obj
defines values one
, two
as well as three
to five
optionally, we lose the benefits of getting back exact types, so to solve this we can introduce a new type, ObjRequired
that includes only the keys that are always present, and use this to intersect with our mapped type.
type ObjRequired = {
one: number;
two: number;
};
type MapToReturn<T extends ObjOptional> = ObjRequired & {
[K in keyof T as T[K] extends true ? K : never]: number;
};
This will now produce a type that works as expected, but having an intersection type is not always ideal to work with compared to a simple object like type. So instead of producing an intersection we can further refine our mapped type to include both the keys from ObjRequired
and those true
value entries from our generic:
type MapToReturn<T extends ObjOptional> = {
[K in keyof T | keyof ObjRequired as K extends keyof T
? T[K] extends true
? K
: never
: K]: number;
};
The approach is mostly similar, we create a union |
between the keys of our generic and those from ObjRequired
. Because of this we have to check the key, K
belongs to our generic T
before indexing into its value T[K]
to check whether it is true
. This is why we have a nested conditional. We can leave the value type as number
if we don't need to perform any further checks or operations on it.
The last thing is to introduce the generic into the function, this is fairly straightforward, if you don't mind the implicit return type you can type the generic on the axios get
call as the return type we've built:
function getRemoteObj<T extends ObjOptional>(id: number, whatToInclude: T) {
return axios.get<MapToReturn<T>>(`api/obj/${id}`, {
params: whatToInclude,
});
}
Hope that helps, here's a link to a typescript playground.