Inference of Generic Types in TypeScript

I've implemented a basic messaging system in TypeScript using implicit anys but now I'm struggling to properly type it so that no type information is lost.

These messages are simple objects containing data used by handler functions, with a message.type property determining which handler function to call.

There's a base interface called Message that only includes the type property, along with more specific interfaces that extend from it.

I'm having trouble figuring out the correct way to type this, as the compiler is throwing an error:

Type '(message: MessageA) => void' is not assignable to type 'MessageHandler'.
  Types of parameters 'message' and 'message' are incompatible.
    Type 'T' is not assignable to type 'MessageA'.
      Property 'x' is missing in type 'Message' but required in type 'MessageA'.

Here's a simplified version of the code that reproduces the issue:

export enum MessageType {
  MessageTypeA,
  MessageTypeB,
}

export interface Message {
  readonly type: MessageType
}

export interface MessageA extends Message {
  readonly type: MessageType.MessageTypeA
  readonly x: string
}

export interface MessageHandler {
  <T extends Message>(message: T): void
}

const onMessageA: MessageHandler = (message: MessageA) => {
  console.log(message.x)
}

While other parts of the system exist, they aren't directly related to this issue.

Given how the system operates, TS needs to infer the generic type. Declaring MessageHandler like below won't work:

export interface MessageHandler<T extends Message> {
  (message: T): void
}

I tested this code using TypeScript versions 3.8.3 and 3.9.2.

Here's a link to play around with this code in the TypeScript Playground: link.

I also attempted declaring MessageHandler differently, but encountered the same error:

export type MessageHandler = <T extends Message>(message: T) => void

How can I properly type MessageHandler to accept any message as long as it has a type property, without explicitly specifying the type during the function call?

EDIT

To provide context, I use the MessageHandler like this:

const defaultFallback = <T extends Message>(message: T) => console.warn('Received message with no handler', message)


export type MessageHandlers = {
  readonly [P in MessageType]?: MessageHandler;
}

export const makeHandler = (functions: MessageHandlers, fallback: MessageHandler = defaultFallback) => (message: Message) => {
  if (!message)
    return

  const handler = functions[message.type]

  if (handler)
    handler(message)
  else if (fallback)
    fallback(message)
}

const onMessageA: MessageHandler = (message: MessageA) => {
  console.log(message.x)
}

const onMessageB: MessageHandler = (message: MessageB) => {
  ...
}

makeHandler({
  [MessageType.MessageA]: onMessageA,
  [MessageType.MessageB]: onMessageB,
})

Answer №1

Your request is not compatible with type safety, and you'll have to rely on many `any` or other type assertions to make it work. The issue lies in the fact that `onMessageA` and `onMessageB` only accept messages of type `MessageA` and `MessageB`, respectively. If you try to label them as a type that should "accept any message as long as it has a `type` property," you will receive a compiler warning. The suitable type for those handlers is the version that you mentioned was not an option, where `MessageHandler<T>` itself is generic:

export interface MessageHandler<T extends Message> {
  (message: T): void;
}

You can then manually add annotations to them:

const onMessageA: MessageHandler<MessageA> = message => {
  console.log(message.x);
};

Or you can utilize a helper function that allows the compiler to infer the type of `T` for you:

// Helper function for type inference
const oneHandler = <T extends Message>(h: MessageHandler<T>) => h;

// onMessageA will be inferred as a MessageHandler<MessageA>:
const onMessageA = oneHandler((message: MessageA) => {
  console.log(message.x);
});

Given your use case of constructing a handler that can manage anything from a discriminated union of `Message` types created from various handlers, you can use the generic `MessageHandler<T>` to define this process. First, we require the complete discriminated union as a type:

type Messages = MessageA | MessageB;

Then, you can create a `makeHandler()` function that receives a mapping from `MessageType` to individual handlers:

function makeHandler(
  map: {
    [P in Messages["type"]]: MessageHandler<Extract<Messages, { type: P }>>
  }
): MessageHandler<Messages> {
  return <M extends Messages>(m: M) => (map[m.type] as MessageHandler<M>)(m);
}

The input type `[P in Messages["type"]]: MessageHandler<Extract<Messages, { type: P }>>` is similar to:

{
  [MessageType.MessageTypeA]: MessageHandler<MessageA>;
  [MessageType.MessageTypeB]: MessageHandler<MessageB>;
};

This is what you need to pass in. The output type `MessageHandler<Messages>` represents a handler for the entire union.

Although the implementation at runtime would appear like `m => map[m.type](m)`, the compiler cannot verify its type safety due to high-order inference. An alternative is to use a redundant yet type-safe implementation:

return (m: Messages) => m.type === MessageType.MessageTypeA ? map[m.type](m) : map[m.type](m);

In conclusion, you should be able to generate and utilize a full handler without having to annotate the specific handler types manually by utilizing existing individual handlers produced by `oneHandler()`, or by creating them directly in the object passed to `fullHandler()`:

const fullHandler = makeHandler({
  [MessageType.MessageTypeA]: m => console.log(m.x),
  [MessageType.MessageTypeB]: m => console.log(m.y)
});

fullHandler({ type: MessageType.MessageTypeA, x: "" }); // Okay
fullHandler({ type: MessageType.MessageTypeA, y: "" }); // Error
fullHandler({ type: MessageType.MessageTypeB, x: "" }); // Error
fullHandler({ type: MessageType.MessageTypeB, y: "" }); // Okay

It seems all set to me. Best of luck with your project!

Playground link to code

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Angular Delight: Jaw-Dropping Animation

After setting up my first Angular project, I wanted to incorporate Angular Animations to bring life to my content as the user scrolls through the page. I aimed to not only have the content appear on scroll but also implement a staggering animation effect. ...

What could be causing the Typescript error when utilizing useContext in combination with React?

I am currently working on creating a Context using useContext with TypeScript. I have encapsulated a function in a separate file named MovieDetailProvider.tsx and included it as a wrapper in my App.tsx file. import { Context, MovieObject } from '../in ...

Dealing with the situation when the assigned expression type number | undefined cannot be assigned to type number

Here is the code for a particular class: id: number; name: string; description: string; productsSet: Set<Products>; constructor( id?: number, name?: string, description?: string, productsSet?: Set<Products> ) { this.id = id; ...

Testing a function within a class using closure in Javascript with Jest

Currently, I am attempting to simulate a single function within a class that is declared inside a closure. const CacheHandler = (function() { class _CacheManager { constructor() { return this; } public async readAsPromise(topic, filte ...

A guide on obtaining the ReturnType within a UseQuery declaration

I am currently working on building a TypeScript wrapper for RTKQ queries that can be used generically. I have made progress on most of my goals, but I am encountering an issue with determining the return type for a generic or specific query. I have shared ...

What causes error TS2345 to appear when defining directives?

Attempting to transition an existing angular application to typescript (version 1.5.3): Shown below is the code snippet: 'use strict'; angular.module('x') .directive('tabsPane', TabsPane) function TabsPane(ite ...

Tips for inputting transition properties in Material UI Popper

Currently, I am making use of material ui popper and I would like to extract the transition into a separate function as illustrated below: import React from 'react'; import { makeStyles, Theme, createStyles } from '@material-ui/core/styles& ...

Sort through nested objects

Trying to filter an array of movies by genre using a function but encountering a TypeError: TypeError: movie.genres.some is not a function. (in 'movie.genres.some(function(item){return item.name === genre;})', 'movie.genres.some' is und ...

Showing a whole number formatted with exactly three decimal places in Angular

I am working on an Angular project that includes an input field for users to enter numbers. My goal is to show the number with exactly 3 decimal places if the user submits a whole number. For instance, if the user inputs 6, I want it to be displayed as 6.0 ...

Node_modules folder is excluded from Typescript compilation

I am struggling to understand why TypeScript is not compiling code from the node_modules folder. Below is the content of my tsconfig.json file: { "compilerOptions": { "rootDir": ".", "baseUrl": ".", "paths": { "shared": ["./src/shared ...

Splitting Angular modules into separate projects with identical configurations

My Angular project currently consists of approximately 20 different modules. Whenever there is a code change in one module, the entire project needs to be deployed. I am considering breaking down my modules into separate projects for individual deployment. ...

Encountering issues with integrating interactjs 1.7.2 into Angular 8 renderings

Currently facing challenges with importing interactive.js 1.7.2 in Angular 8. I attempted the following installation: npm install interactjs@next I tried various ways to import it, but none seemed to work: import * as interact from 'interactjs'; ...

Obtaining the identifier of a generic type parameter in Typescript

Is there a method in TypeScript to retrieve the name of a generic type parameter? Consider the following method: getName<T>(): string { .... implement using some operator or technique } You can use it like this: class MyClass{ } getName< ...

Tips for importing modules without encountering errors

Currently in the process of constructing a compact website with socket.io and express. Opting for Typescript to ensure accurate type errors, then transpiling the frontend code to Javascript for seamless browser execution. Frontend code: import { io, Socke ...

Encountering TypeScript error TS2769 when using Material UI's TableRow with a Link component

Currently, I am in the process of developing a React Single Page Application using Typescript and Material UI. One of my objectives is to include a table where each row acts as a clickable link. The Link component within this table is derived from React Ro ...

Tips for calculating the total sum of inner object property values using JavaScript, TypeScript, or Angular 5

What is the total sum of successCount values in the given array object? var successCount;//I want count of all successCount attributes from the below object var accordianData = [ { name: "Start of Day", subItemsData: [ { title: " ...

Angular 7 error: Form control with name property does not have a valid value accessor

Currently, I am utilizing angular 7 and have a parent and child component set up as demonstrated in the Stackblitz link provided below. Strangely enough, when I assign the formControlName from the child component using "id", everything functions flawlessly ...

Retrieve a specific data point from a web API using Angular framework

Objective: How can I retrieve the specific value "test" in Angular? Issue: An error message is being displayed. Error: SyntaxError: Unexpected token e in JSON at position 1 at JSON.parse () Which syntax element am I missing? ASP.NET // Retrieve "tes ...

Encountering an error while using TypeScript, Mocha, and Express: "TypeError: app.address is not a

While transitioning an API from ES6 to TypeScript, a particular issue arises when attempting to run unit tests on the Express REST Endpoints: TypeError: Cannot read property 'address' of undefined The server code has been slightly adjusted for ...

Warning in Typescript: potential undefined access detected when strict mode is enabled

When using Typescript with "strict": true in the tsconfig.json, a common issue arises where warnings are not triggered for potentially undefined properties, as shown by this code snippet: let x: any = { test: false } let y = x.asdf // no warning issued ...