Home > Mobile >  How to add dynamically resolve property type based on another property value in TypeScript?
How to add dynamically resolve property type based on another property value in TypeScript?

Time:11-25

I have used Polymorphic relationships. So, right now I have the following TypeScript interface:

interface SubjectA {}
interface SubjectB {}
interface SubjectC {}

enum SubjectType {
  SubjectA = 'Subject A',
  SubjectB = 'Subject B',
  SubjectC = 'Subject C',
}

interface ExampleSubject {
  type: SubjectType;
  subject: SubjectA | SubjectB | SubjectC
}

In this example, you can see, ExampleSubject.subject has three possible subject types (SubjectA, SubjectB SubjectC). Now here I want it should resolve its type dynamically. For example, if ExampleSubject.type is SubjectType.SubjectA in that case ExampleSubject.subject should be SubjectA.

Please guide me, How can I resolve this? Thanks

CodePudding user response:

This can be achieved using a discriminated union type (docs)

interface SubjectA { someVar: 'a' }
interface SubjectB { someVar: 'b' }
interface SubjectC { someVar: 'c' }

enum SubjectType {
  SubjectA = 'Subject A',
  SubjectB = 'Subject B',
  SubjectC = 'Subject C',
}

interface ExampleSubjectA {
  type: SubjectType.SubjectA;
  subject: SubjectA
}
interface ExampleSubjectB {
  type: SubjectType.SubjectB;
  subject: SubjectB
}
interface ExampleSubjectC {
  type: SubjectType.SubjectC;
  subject: SubjectC
}

type ExampleSubject = ExampleSubjectA | ExampleSubjectB | ExampleSubjectC
const subjectA: SubjectA = {
  someVar: 'a'
}
const subjectB: SubjectB = {
  someVar: 'b'
}

const exampleSubjectValid: ExampleSubject = {
  type: SubjectType.SubjectA,
  subject: subjectA
}
const exampleSubjectInvalid: ExampleSubject = {
  type: SubjectType.SubjectA,
  subject: subjectB
}
/* Type '{ type: SubjectType.SubjectA; subject: SubjectB; }' is not assignable to type 'ExampleSubject'.
  The types of 'subject.someVar' are incompatible between these types.
    Type '"b"' is not assignable to type '"a"'.(2322) */

TypeScript playground

Note: The SubjectX interface definitions need to differ in some way otherwise TypeScript will collapse them into the same concept and they will effectively be three aliases for the same interface (and therefore can not be discriminated). This is why someVar was included in the example / TS playground link

CodePudding user response:

For that, you'd use a discriminated union rather than an interface. You already have the discriminant (SubjectType). Here's how you'd write the union:

type ExampleSubject =
    | { type: SubjectType.SubjectA; subject: SubjectA }
    | { type: SubjectType.SubjectB; subject: SubjectB }
    | { type: SubjectType.SubjectC; subject: SubjectC };

Then if you had x which was of type ExampleSubject, you could narrow the type of x.subject by checking x.type:

if (x.type === SubjectType.SubjectA) {
    console.log(x.subject);
    //             ^? (property) subject: SubjectA
}

Playground example

Note that I've put something inside SubjectA, SubjectB, and SubjectC so TypeScript can tell them apart. Since TypeScript's type system is structural, not nominal, when doing sample types like the above it's important to put something in the types to differentiate them from each other. Otherwise, they're compatible with each other, which can take you down the wrong path...

  • Related