Search code examples
typescripttypestype-inferencetype-safety

TypeScript infer type by its unique field


I have a composite type called IncomingMessage

export type IncomingMessage =
  | MessageText
  | MessageGameStarted
//| ... rest

export type IncomingMessageType = 
  | IncomingMessage['messageType']

each member of type IncomingMessage is represented as an interface with unique key messageType

interface MessageText {
  messageType: MessageType.TEXT
  content: string
}

interface MessageGameStarted {
  messageType: MessageType.GAME_STARTED
}

// ... rest

Then, I have a class Network that listens for those messages and delegates them to appropriate handlers. You can add handlers to Network depending on IncomingMessageType

type Handler<T extends IncomingMessage> = (message: T) => any
type Handlers = {[messageType in IncomingMessageType]?: Handler</*???*/>}

class Network {
    private readonly handlers: Handlers = {}
    
    on<T extends IncomingMessage>(messageType: T['messageType'], handler: Handler<T>) {
        this.handlers[messageType] = handler
        return this
    }
}

What I need to accompish:

  1. method Network.on(messageType, handler) should be type-safe: when you type
network.on(MessageType.TEXT, message => {
   // message type should be MessageText
})

typescript should infer type of 'message' parameter based on 'messageType' parameter

  1. I should properly type Handlers. I want to be able to infer Handler generic from current messageType

Solution

  • You'll need to extract the right message from the union with Extract:

    type Handler<T extends IncomingMessage> = (message: T) => any
    type Handlers = { [messageType in IncomingMessageType]?: Handler<Extract<IncomingMessage, { messageType: messageType }>>}
    
    class Network {
        private readonly handlers: Handlers = {}
        
        on<T extends IncomingMessageType>(messageType: T, handler: Handler<Extract<IncomingMessage, { messageType: T }>>) {
            this.handlers[messageType] = handler as Handler<any>;
            return this
        }
    }
    

    You also have to change the signature of on to use the generic on the message type instead, as TypeScript cannot infer the message's type in the handler.

    So now your on should work correctly:

    new Network().on(MessageType.TEXT, message => {
        message
    //  ^?
    });
    

    However you will notice that I have used an assertion when assigning the handler, and I don't think it is possible to get around it.

    Playground