简体   繁体   中英

Enforce single required property on Typescript interface, allow others

I am creating a generic WebSocket component. I want to require the type Message to have a required property called type .

export type Message = {
  type: string;
  [key: string]: any; // eslint-disable-line @typescript-eslint/no-explicit-any
};

The component is given a series of callback functions that are called when a Message of the appropriate type is called.

export interface Listeners {
  [type: string]: (msg: Message) => void;
}

Code snippet of WebSocket implementation:

...
ws.onmessage = (event: MessageEvent): void => {
  console.log("[websocket] got raw message", event.data);
  try {
    const msg = JSON.parse(event.data);

    if (listeners[msg.type]) {
      console.log("[websocket] got message", msg);
      listeners[msg.type](msg);
    }
  }
}
...

When using the WebSocket component, I'd like to define a custom type is an extension of the Message type, and includes the type property.

interface GraphMessage extends Message {
  id: string;
  type: "initial" | "update";
  chartType: "line" | string;
  data: GraphPoint[];
}

I'm trying to use the component like so:

const handleUpdate = (msg: GraphMessage) => {}
const handleInitial = (msg: GraphMessage) => {}

const ws = await websocket("ws://localhost:9999/", {
  initial: handleInitial,
  update: handleUpdate
});

However, I'm received a Typescript error:

TS2322: Type '(msg: GraphMessage) => void' is not assignable to type '(msg: Message) => void'. 
  Types of parameters 'msg' and 'msg' are incompatible. 
    Type 'Message' is not assignable to type 'GraphMessage'.

How can I make Message assignable to type GraphMessage ?

Edit: I believe I've found a solution which is to make Message a generic type.

type Message<T> = {
    type: string
    [key: string]: any
} & T

interface GraphMessage {
    graphName: string
}

type Callback = (msg: Message<GraphMessage>) => void

const myBaseMessage = {
    t...

Playground Link

TypeScript is right to point out that Type '(msg: GraphMessage) => void' is not assignable to type '(msg: Message) => void' .

If your handler depends on GraphMessage but you provide just a Message , this can result in runtime exceptions:

// Can, but doesn't have to include `id`
export type Message = {
    type: string;
    [key: string]: any;
};

interface GraphMessage extends Message {
  id: string;
  type: "initial" | "update";
}

const message: Message = {
    type: 'foo',
}

const handler = (msg: GraphMessage): void => {
    console.log(msg.type.toLocaleLowerCase());
}

If we do

handler(message); // Compile-time error

TypeScript will prevent an exception from being thrown.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM