How to make generic type in typescript that deals with optional includes from API response?

Viewed 259

I've got Laravel backend and I use transformers. That means that I can type ?include=reviews_count,author, etc. to get additional data from backend. Is there any way to statically type it?

One way is, of course, to create one DTO type and all includes would be optional, but that's not ideal.

Other (the one I came up with) is to have some kind of generic type that would take DTO as a first argument and union of include types as a second argument. However I ran into Typescript design limitation issue because Typescript doesn't work this kind of indexing by a generic type.

The last thing I came up with to have some kind of Includes type that looks like this

export type Includes<BaseClass, Includes> = {
  [T in keyof BaseClass]: BaseClass[T];
} &
  {
    [T in keyof Includes]: Includes[T];
  };

type TeacherDTO = {
  name: string;
  surname: string;
}

type TeacherIncludeNumber = {
  count: number;
}

type GetTeachersResponse = Includes<TeacherDTO, TeacherIncludeMember | ...>;

But it does not really work. Is there any reliable way to handle it?

2 Answers

Well, you need a few things to do. Make your TeachDto type to include all possible properties. And then utilize Pick<T,R> type map in your function's return type like below :

type TeacherDTO = {
    name: string;
    surname: string;
    number: string;
    gender: string;
    birthDate: Date
}

type TeacherKeys = keyof TeacherDTO;


function getTeacher<T extends TeacherKeys>(keys: T[]): Pick<TeacherDTO, T> {
     // implementation omitted for brevity
    return {} as any;
}


const finalTeacher = getTeacher(["name","number"]);

finalTeacher.name // ok
finalTeacher.number // ok
finalTeacher.gender // not ok



getTeacher(["foo"]) // not ok

Playground Link

First you can define your includes and basic teacher dto

type Includes = {
    count: number,
    author: { name: string, age: number }
}

type TeacherDTO = {
  name: string;
  surname: string;
}

Then you can make your method require one or more of your includes and based on those includes your final datatype would be defined (please note that Unarray was taked from here)

type Unarray<T> = T extends Array<infer U> ? U : T;

function getTeacher<TIncludes extends Array<keyof Includes>>(...args: TIncludes): TeacherDTO & Pick<Includes, Unarray<TIncludes>> {
    // your logic
    return {} as any;
}

var a = getTeacher();
a.name // works
a.surname // works
a.count // doesnt work
a.author // doesnt work

var b = getTeacher('author');
b.name // works
b.surname // works
b.author // works
b.count // doesnt work

var c = getTeacher('author', 'count');
c.name // works
c.surname // works
c.author // works
c.count // works

Playground link is here.

Cheers.

Related