Nir
Nir

Reputation: 175

TypeScript Generic infer based on field

enum Letter {
  A = "A",
  B = "B",
  C = "C"
}

export type EntityT<T extends Letter = Letter> = T extends Letter.A
  ? { a: number }
  : T extends Letter.B
  ? { b: string }
  : T extends Letter.C
  ? { c: string }
  : never;

type DeltaT<T extends EntityT> = _DeltaT<T, keyof T>;

export type _DeltaT<E extends EntityT, K extends keyof E> = {
  key: K;
  oldValue: E[K];
  newValue: E[K];
};

export type ProblemT<T extends Letter = Letter> = EntityT<T> extends infer E
  ? {
      id: string;
      type: T;
      delta: DeltaT<E>[];
      oldEntity: E;
      newEntity: E;
    }
  : never;

const testCase: ProblemT<Letter.A> = {
  id: "id",
  type: Letter.A,
  delta: [{ key: "a", oldValue: 1, newValue: 2 }],
  oldEntity: { a: 1 },
  newEntity: { a: 2 }
};

function myFunc<T extends Letter>(e: ProblemT<T>) {
  switch (e.type) {
    case Letter.A: {
      const uselessConversion = e as ProblemT<Letter.A>;
      return uselessConversion.newEntity.a;
    }
    case Letter.B: {
      const uselessConversion = e as ProblemT<Letter.B>;
      return uselessConversion.newEntity.b;
    }
    case Letter.C: {
      const uselessConversion = e as ProblemT<Letter.C>;
      return uselessConversion.newEntity.c;
    }
  }
}

I want to declare a type ProblemT that depends on his type key, will return different values in oldEntity and newEntity. I want to run myFunc with the switch without the need to convert the object manually, and TypeScript will infer the type based on the type property. But it does not work well. How to make the switch infer the type automatically without the need to convert. Is there a better way to declare the ProblemT differently type so the switch can infer it?

Here is a link to the TypeScript playground

Upvotes: 1

Views: 147

Answers (1)

Mike Jerred
Mike Jerred

Reputation: 10565

Note that types such as Letter.A | Letter.B also extends Letter, so the compiler does not know that things are as constrained as they are.

Something like this will work:

enum Letter {
  A = "A",
  B = "B",
  C = "C"
}

export type EntityT<T extends Letter> =
  T extends Letter.A ? { a: number }
  : T extends Letter.B ? { b: string }
  : T extends Letter.C ? { c: string }
  : never;

export type DeltaT<T extends Letter, K = keyof EntityT<T>> = K extends keyof EntityT<T>
  ? {
    key: K;
    oldValue: EntityT<T>[K];
    newValue: EntityT<T>[K];
  }
  : never;

export type ProblemT<T extends Letter> = {
  id: string;
  type: T;
  delta: DeltaT<T>[];
  oldEntity: EntityT<T>;
  newEntity: EntityT<T>;
};

function myFunc(e: ProblemT<Letter.A> | ProblemT<Letter.B> | ProblemT<Letter.C>) {
  switch (e.type) {
    case Letter.A: return e.newEntity.a;
    case Letter.B: return e.newEntity.b;
    case Letter.C: return e.newEntity.c;
  }
}

Upvotes: 4

Related Questions