Home > Software engineering >  Is there a way to replace a generic type by another type inside an interface?
Is there a way to replace a generic type by another type inside an interface?

Time:08-02

I have an file with those interfaces :

export type Flatten<T> = T extends any[] ? T[number] : T;

export interface IGeneric<T> {
  data: T extends any[] ? T : Flatten<T> | null;
  meta: IGenericMeta;
}

export type IGenericData<T> = Pick<IGeneric<T>, 'data'>;

export interface IGenericMeta {
  pagination?: {
    page: number;
    pageSize: number;
    pageCount: number;
    total: number;
  };
}

// I use it like that :

interface ITest {
  id: number;
  attributes: {
    field1: string;
    relation1: IGenericData<ITest2>;
  }
}

interface ITest2 {
  id: number;
  attributes: {
    field1: string;
  }
}

When I retrieve the data all's good, but for insertion I can't use this anymore I need to set the field relation1 to number | null.

If I set the relation like this :


// ...
interface ITest {
  id: number;
  attributes: {
    field1: string;
    relation1: IGeneric<ITest2> | number | null;
  }
}

const test: IGeneric<ITest> = {
  meta: {},
  data: {
    id: 1,
    attributes: {
      field1: '',
      relation1: {
        data: {
          id: 2,
          attributes: {
            field1: 'test',
          },
        },
      },
    },
  },
};
                                      ↓ error here
if (test.data?.attributes?.relation1?.data?.id) {
}

Typescript yell at me :

Property 'data' does not exist on type 'number | IGenericData<ITest2>'.
  Property 'data' does not exist on type 'number'.ts(2339)

I could setup two interfaces who extends the base one like this:

interface ITestBase {
  id: number;
  attributes: {
    field1: string;
  }
}

type ITestInsert = ITestBase & {
  attributes: {
    relation1: number | null;
  }
}

interface ITestRetrieve = ITestBase & {
  attributes: {
    relation1: IGeneric<ITest2>;
  }
}

But I don't really like this setup because I already have a lot of interfaces and I don't what to spend a whole week to refactor this way.

I wanted to find a way with a generic type (maybe?) to override the behaviour I setup earlier to replace all IGenericData with number | null

type Replace<T> = ???

type Test = Replace<ITest['attributes']>;

Maybe this question was already answered but I didn't find anything related to what i want to do. Sorry in advance if there's already an answer.

EDIT 1:

playground typescript updated

CodePudding user response:

Caveat: This sort of deep type transformation often has lots of edge cases, so you should test any suggested solution thoroughly against your use cases and be prepared to modify or abandon it based on the results. I will answer based on the examples in your question.


It looks like you want Replace<T> to be a recursive conditional type; if T is some GenericData, then you will replace it with number | null. If T is a primitive type, you want to leave it alone. Otherwise if T is some object type, you want to recurse down into it and map every property to the Replaced version of the property.

That approach translates to this code:

type Replace<T> = T extends IGenericData<any> ? (number | null) :
  T extends object ? { [K in keyof T]: Replace<T[K]> } : T;

Let's see if it works:

type ReplacedITestAttributes = Replace<ITestAttributes>;
/* type ReplacedITestAttributes = {
    field1: string;
    relation1: number | null;
} */

type ReplacedITest = Replace<ITest>;
/* type ReplacedITest = {
    id: number;
    attributes: {
        field1: string;
        relation1: number | null;
    };
} */

Looks good!

Playground link to code

  • Related