Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

TypeScript type inference/narrowing challenge

I'm currently trying to improve the types on some existing code. My code looks roughly like this:

/* dispatcher.ts */
interface Message {
    messageType: string;
}

class Dispatcher<M extends Message> {    
    on<
        MessageType extends M["messageType"],
        SubMessage extends M & { messageType: MessageType }
    >(
        messageType: MessageType,
        handler: (message: SubMessage) => void
    ): void { }
}

/* messages.ts */
interface AddCommentMessage {
    messageType: "ADD_COMMENT";
    commentId: number;
    comment: string;
    userId: number;
}

interface PostPictureMessage {
    messageType: "POST_PICTURE";
    pictureId: number;
    userId: number;
}

type AppMessage = AddCommentMessage | PostPictureMessage;

/* app.ts */
const dispatcher = new Dispatcher<AppMessage>();

dispatcher.on("ADD_COMMENT", (message: AddCommentMessage ) => {
                                    /* ^^ REMOVE THIS TYPE HINT!*/
    console.log(message.comment);
});

I'd like to remove the need to explicitly narrow the type of the message passed to the message handler (where /*REMOVE THIS TYPE HINT!*/ is), such that it correctly narrows to the type that has a matching messageType type (e.g. if messageType is "ADD_COMMENT" then message should be AddCommentMessage).

If this is not possible right now please let me know. I'm under the impression that it isn't but I'm not quite sure.

like image 538
Pat Avatar asked Mar 20 '17 21:03

Pat


People also ask

What is type narrowing in TypeScript?

TypeScript follows possible paths of execution that our programs can take to analyze the most specific possible type of a value at a given position. It looks at these special checks (called type guards) and assignments, and the process of refining types to more specific types than declared is called narrowing.

Should I let TypeScript infer types?

Letting TS infer will give you more freedom when refactoring types and it makes your code more flexible. This is the main reason why most of the time I recommend against annotating loose variables or callback/promise arguments.

Does TypeScript have type inference?

TypeScript infers types of variables when there is no explicit information available in the form of type annotations. Types are inferred by TypeScript compiler when: Variables are initialized. Default values are set for parameters.

What would be the best common type inferred by TypeScript?

The best common type algorithm In this case, TypeScript selects the number array type ( number[] ) as the best common type. In this example, TypeScript infers the type for arr to be (RegExp | Date)[] .


1 Answers

It's not possible, unless you are willing to change the code quite a bit.

Your base interface

interface Message {
    messageType: string;
}

is too general, I think messageType: string precludes any inference based on the value of the messageType, and looks like it's impossible to sufficiently narrow it in the Dispatcher interface.

If you limit the code to AppMessage and its descendants only, here is an example how to make typescript to infer the types you need, guided by string literal types (keyof AppMessageMap is actually a union of string literal types "ADD_COMMENT" | "POST_PICTURE"):

/* dispatcher.ts */

class Dispatcher {    
    on<
        MessageType extends keyof AppMessageMap
    >(
        messageType: MessageType,
        handler: (message: AppMessageMap[MessageType] & {messageType: MessageType}) => void
    ): void { }
}

/* messages.ts */
interface AddCommentMessage {
    commentId: number;
    comment: string;
    userId: number;
}

interface PostPictureMessage {
    pictureId: number;
    userId: number;
}

interface AppMessageMap {
    "ADD_COMMENT": AddCommentMessage,
    "POST_PICTURE": PostPictureMessage
}
type AppMessage = AppMessageMap[keyof AppMessageMap];

/* app.ts */
const dispatcher = new Dispatcher();


dispatcher.on("ADD_COMMENT", (message) => {
    console.log(message.comment);
});

I also removed messageType property from the interfaces to avoid duplication, I think that intersection type in handler parameter achieves the same effect.

like image 103
artem Avatar answered Sep 28 '22 21:09

artem