Determining the generic type from supplied Record values in TypeScript: A guide

There is a function called polymorficFactory that creates instances of classes based on a provided 'discriminator' property:

type ClassConstructor<T> = {
    new (...args: any[]): T;
};
type ClassMap<T> = Record<string, ClassConstructor<T>>;

function polymorficFactory<T extends object>(
  classMap: ClassMap<T>,
  discriminator: string,
  input: Record<string, any>,
): T {
  if (!input[discriminator]) throw new Error('Input does not have a discriminator property');

  const discriminatorValue = input[discriminator];
  const constructor = classMap[discriminatorValue];

  return plainToInstance(constructor, input); // class-transformer util
}

This function is useful for handling unknown objects from request payloads:

const MOCK_PAYLOAD = {
  type: 'SOME_DTO',
  someProperty: 1,
  someStr: 'Lorem',
  someNestedProp: {
    someBool: true,
  }
} as as Record<string, unknown>; // Record<string, unknown/any>, since comes from JSON payload, but will always be an object
const dto = polymorficFactory<SomeDto | SomeOtherDto>(
  {
    SOME_DTO: SomeDto,
    SOME_OTHER_DTO: SomeOtherDto,
  },
  'type',
  MOCK_PAYLOAD ,
);

dto; // when hovering, type will be "SomeDto | SomeOtherDto"

If type parameters are not provided:

const dto = polymorficFactory(
  ...
);

dto; // when hovering, type will be "SomeDto"

The function selects the first value it finds in the provided object:

{
  SOME_DTO: SomeDto,
  SOME_OTHER_DTO: SomeOtherDto,
}

Is there a way to infer the union type, SomeDto | SomeOtherDto or any other Dto classes from the map values, without explicitly specifying it?

For example, with this classMap:

{
  A: aDto,
  B: bDto,
  C: cDto,
}

The expected inferred union type for the result would be:
aDto | bDto | cDto

Playground link

Answer №1

To enhance your understanding of inferring function arguments, it's important to keep in mind the following rule: When you aim to deduce a value, you must replicate all transformations that occur at runtime within the type scope.

For instance, if you are constructing a class instance, you should utilize the built-in type InstanceType to obtain the type of the class instance.

It's advisable to make these class maps immutable by using the as const assertion.

Take this into consideration:

import { plainToInstance } from "class-transformer";

type ClassConstructor<T> = {
  new(...args: any[]): T;
};

function polymorphicFactory<
  Inst,
  Values extends PropertyKey,
  Discriminator extends string,
  ClassMap extends Record<Values, ClassConstructor<Inst>>,
  Input extends Record<Discriminator, Values>
>(
  classMap: ClassMap,
  discriminator: Discriminator,
  input: Input,

): InstanceType<ClassMap[Input[Discriminator]]>
function polymorphicFactory<
  Inst,
  Values extends PropertyKey,
  Discriminator extends string,
  ClassMap extends Record<Values, ClassConstructor<Inst>>,
  Input extends Record<Discriminator, Values>
>(
  classMap: ClassMap,
  discriminator: Discriminator,
  input: Input,

) {
  if (!input) throw new Error('Input is not an object');
  if (!input[discriminator]) throw new Error('Input does not have a discriminator property');

  const discriminatorValue = input[discriminator];
  const klass = classMap[discriminatorValue];
  const result = plainToInstance(klass, input)

  return result
}

class SomeDto {
  tag = 'SomeDto'
}

class SomeOtherDto {
  tag = 'SomeOtherDto'
}

const CLASS_MAP = {
  SOME_DTO: SomeDto,
  SOME_OTHER_DTO: SomeOtherDto,
} as const

const MOCK = {
  type: 'SOME_OTHER_DTO',
  someProperty: 1,
} as const

const explicitParams = polymorphicFactory(
  CLASS_MAP,
  'type',
  MOCK,
);

explicitParams; // type will be " SomeOtherDto" when hovering

Playground

Notice how each value in polymorphicFactory is correctly inferred.

  1. Pay attention to this line
    const discriminatorValue = input[discriminator];
    . If you expect discriminator to be a valid index for input, you should apply the proper constraint:
function polymorphicFactory<
  // .....
  Values extends PropertyKey,
  Discriminator extends string,
  Input extends Record<Discriminator, Values>
>(
  // ...
  discriminator: Discriminator,
  input: Input,
) 

The variable input should extend

Record<Discriminator, Values>
, indicating that discriminator is an allowable index for input.

Consider this line

const klass = classMap[discriminatorValue];

The value of discriminatorValue from input[discriminator] must be a valid key for classMap, requiring an appropriate constraint:

function polymorphicFactory<
  Inst,
  Values extends PropertyKey,
  Discriminator extends string,
  ClassMap extends Record<Values, ClassConstructor<Inst>>,
  Input extends Record<Discriminator, Values>
>(
  classMap: ClassMap,
  discriminator: Discriminator,
  input: Input,

)

Observe the Values generic, which acts as a value in Input and a key in ClassMap. This distinction is crucial.

Now, with all arguments inferred, the return type needs to be inferred. Our return type is an instance of classMap[input[discriminator]] or

InstanceType<ClassMap[Input[Discriminator]]>

For further insights on inferring function arguments, you can refer to my article

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

Using TypeScript to call Node.js functions instead of the standard way

Can someone assist me with the issue I'm facing? I have developed a default node.js app with express using Visual Studio nodejs tools, and now I am attempting to call the setTimeout function that is declared in node.d.ts. The code snippet in question ...

Utilizing a segment of one interface within another interface is the most effective method

In my current project using nextjs and typescript, I have defined two interfaces as shown below: export interface IAccordion { accordionItems: { id: string | number; title: string | React.ReactElement; content: string | React. ...

Incorporate a personalized add-button into the material-table interface

My current setup includes a basic material-table structured like this: <MaterialTable options={myOptions} title="MyTitle" columns={state.columns} data={state.data} tableRef={tableRef} // Not functioning properly editabl ...

Enhance Component Reusability in React by Utilizing Typescript

As I embark on developing a React application, my primary goal is to keep my code DRY. This project marks my first experience with Typescript, and I am grappling with the challenge of ensuring reusability in my components where JSX remains consistent acros ...

Remove the export statement after transpiling TypeScript to JavaScript

I am new to using TypeScript. I have a project with Knockout TS, and after compiling it (using the Intellij plugin to automatically compile ts to js), this is my sample.ts file: import * as ko from "knockout"; ko; class HelloViewModel { language: Kn ...

404 Error: Unable to Locate Socket Io

I'm currently working on implementing a chat feature in Angular 2 using Socket IO, following this tutorial. However, I encountered an error message during a test on the server: GET http://localhost:3000/socket.io/?EIO=3&transport=polling& ...

Conceal Primeng context menu based on a certain condition

I'm struggling to prevent the context menu from showing under certain conditions. Despite following the guidelines in this post, the context menu continues to appear. My goal is to implement a context menu on p-table where it should only show if there ...

Guide to dynamically resizing the Monaco editor component using react-monaco-editor

Currently, I am integrating the react-monaco-editor library into a react application for viewing documents. The code snippet below showcases how I have set specific dimensions for height and width: import MonacoEditor from 'react-monaco-editor'; ...

Error in typescript: The property 'exact' is not found in the type 'IntrinsicAttributes & RouteProps'

While trying to set up private routing in Typescript, I encountered the following error. Can anyone provide assistance? Type '{ exact: true; render: (routerProps: RouterProps) => Element; }' is not compatible with type 'IntrinsicAttribu ...

The element is implicitly assigned the 'any' type due to the inability to use an expression of type to index the element

Check out my TS playground here // I have colours const colors = { Red: "Red", Blue: "Blue", Green: "Green" } type TColor = keyof typeof colors; // Some colours have moods associated with them const colorsToMood = { ...

TypeScript does not throw a compiler error for incorrect type usage

In my current setup using Ionic 3 (Angular 5), I have noticed that specifying the type of a variable or function doesn't seem to have any impact on the functionality. It behaves just like it would in plain JavaScript, with no errors being generated. I ...

Experimenting with throws using Jest

One of the functions I'm testing is shown below: export const createContext = async (context: any) => { const authContext = await AuthGQL(context) console.log(authContext) if(authContext.isAuth === false) throw 'UNAUTHORIZED' retu ...

Verifying the format of an object received from an HTTP service using a TypeScript interface

Ensuring that the structure of the http JSON response aligns with a typescript interface/type is crucial for our javascript integration tests against the backend. Take, for example, our CurrentUser interface: export interface CurrentUser { id: number; ...

How can I deploy a react-express application to Azure cloud platform?

Struggling to deploy my react-express application on Azure. The code is divided into client and server directories. Attempted deployment using Azure Static Web application but encountered failure. https://i.stack.imgur.com/ailA0.png https://i.stack.imgur.c ...

Typescript is asserting that the React class component has a property, despite what the component itself may suggest

I'm running into an issue with React refs and class components. Here's my simplified code snippet. I have a component called Engine with a property getInfo. In my test, I check for this.activeElement &&, which indicates that it's no ...

What steps can be taken to prioritize files with specific extensions in webpack?

I have a dilemma with two files: - somefile.scss - somefile.scss.ts When importing the file in my typescript code, it is referencing the wrong one: import styles from "somefile.scss" The typescript part works fine with the correct import (.scss ...

Why did my compilation process fail to include the style files despite compiling all other files successfully?

As English is not my first language, I kindly ask for your understanding with any typing mistakes. I have created a workspace with the image depicted here; Afterwards, I executed "tsc -p ." to compile my files; You can view the generated files here Unf ...

Starting up a pre-existing Angular project on your local machine

I am completely new to Angular and facing difficulties running an existing project on my machine. Despite conducting numerous tests and following various articles, I still cannot get the project to run. Here is the layout of my project files: https://i.s ...

TypeScript properties for styled input component

As I venture into TS, I’ve been tasked with transitioning an existing JS code base to TS. One of the challenges I encountered involves a styled component in a file named style.js. import styled from "styled-components"; export const Container ...

Using a comma as a decimal separator for input is not permitted when working with angular/typescript/html

I am facing an issue with my Angular frontend where I have numerous html input fields that trigger calculations upon typing. <input type="text" [(ngModel)]="myModel.myAttribute" (input)="calculate()"> The problem arise ...