最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

typescript - Function return type based on input parameters - Stack Overflow

programmeradmin0浏览0评论

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
Add a comment  | 

1 Answer 1

Reset to default 3

Yes, 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.

发布评论

评论列表(0)

  1. 暂无评论