Home > OS >  Typescript generic type with discriminator - runtime access to discriminator
Typescript generic type with discriminator - runtime access to discriminator

Time:12-13

I don't think this is possible but thought I'd ask. I have types with discriminator, eg:

type Fish={
  type: "fish"
  canSwim: boolean
}
type Bird={
  type: "bird"
  canFly: boolean
}

I have a state object such as:

const state={
  fish: [/* array of fish */]
  birds: [/* array of birds */]
}

I would like to write a function:

function getFromState<T>():T[] {
  return state[T.type] as T[]
}

Clearly a workaround is to pass the type as a parameter, eg:

function getFromState<T>(type):T[] {
  return state[type] as T[]
}

But then you end up repeating things:

const animal=getFromState<Fish>("fish")

ANOTHER EXAMPLE OF SIMILAR KIND OF THING

I have an api which sends messages over websocket to a remote server. The messages follow the pattern:

{
   cmd: "string command",
   arg1,
   arg2
}

I have a function:

function makeRequest(cmd:string, args) {
   websocket.send(JSON.stringify({
      cmd,
      ...args
   })
}

I would like to be able to strongly type the args parameter based on the value of cmd parameter.

CodePudding user response:

As I think you already know, you can't refer to types at runtime because of type erasure.

There are many alternative paths, but it all depends on your actual use case. It's hard to know what your real needs are based on your question as written. I suggest you rewrite your question with your actual use case (states) rather than the abstract Animal). I'll make a guess as to your needs in this tentative answer, but will update it if you update your question and @ me above or comment below this answer.

In the code below runtime "polymorphism" is achieved with the discriminator property from your example (type) and regular if-else or switch logic. Because Typescript is able to narrow types by inference, you also get compile-time static type safety. There is no DRY violation as in your workaround.

You can test both its static type checking and its runtime behavior in the Playground.

type Fish = {
    type: "fish"
    canSwim: boolean
}
type Bird = {
    type: "bird"
    canFly: boolean
}

type Animal = Fish | Bird

type AnimalTypes = Animal['type']

function getAnimal(type: AnimalTypes): Animal {
    return type === 'fish' ? { type: 'fish', canSwim: true } : { type: 'bird', canFly: true }
}

const state = {
    fish: [{ type: 'fish', canSwim: true }, { type: 'fish', canSwim: false }] as Fish[],
    bird: [{ type: 'bird', canFly: true }, { type: 'bird', canFly: false }] as Bird[]
}

function getFromState(type: AnimalTypes):Animal[] {
  return state[type]
}


function polymorphicFunction(animal: Animal) {
    if (animal.type === 'fish') {
        console.log(`It ${animal.canSwim ? 'can' : 'cannot'} swim!`)
    } else {
        console.log(`It ${animal.canFly ? 'can' : 'cannot'} fly!`)
    }
}


// both static type checking and runtime "polymorphism" works.
// Nothing has been repeated (DRY).
polymorphicFunction(getFromState('fish')[0])
polymorphicFunction(getFromState('bird')[1])

If this isn't good enough, you can achieve real polymorphism (i.e. without the if-else logic above) if you use typed classes. And combined with utility types such as Parameters<T>, you can achieve what you want in your "ANOTHER EXAMPLE OF SIMILAR KIND OF THING" section.

  • Related