Limit the typescript generic type to only a singular string literal value, preventing the use of unions

Within my project, I have introduced a generic entity type that utilizes a generic to determine a field type based on a specific set of string literals:

type EntityTypes = 'foo' | 'bar' | 'baz';

type EntityMappings = {
  foo: string;
  bar: number;
  baz: Array<string>;
}

type GenericEntity<T extends EntityTypes> = {
  type: T;
  fieldProperty: EntityMappings[T];
}

The objective is to mandate all instances of GenericEntity to contain a single type field (consisting of a string literal) that then defines the type of fieldProperty, as shown below:

const instance: GenericEntity<'foo'> = {
  type: 'foo',
  fieldProperty: 'hello',
};

const otherInstance: GenericEntity<'baz'> = {
  type: 'baz',
  fieldProperty: ['a', 'b', 'c'],
}

Nevertheless, due to the fact that T extends EntityTypes permits a union of multiple string literal values from EntityTypes, it becomes possible to execute this undesired action:

const badInstance: GenericEntity<'foo' | 'baz'> = {
  type: 'baz',
  fieldProperty: 'blah',
};

This issue arises because now type embodies 'foo' | 'baz' and fieldProperty is composed of string | Array<string>, thereby disrupting the intended alignment between the two fields.

I am seeking guidance on whether there exists a method to further restrict the generic declaration of GenericEntity to allow only a singular distinct string literal value. Alternatively, is there an alternative approach to ensure that every instance of GenericEntity contains both a type field and a corresponding fieldProperty field?

Answer №1

Currently, there is no direct method to confine a type parameter of a generic to a single member of a union. An open feature request is available at microsoft/TypeScript#27808 suggesting support for something like

T extends <em>oneof</em> EntityTypes
, but it has not been implemented yet. If you would like to see this feature happen, you can visit the issue and give it a thumbs up, although its impact may be limited.

This means that T extends EntityTypes could encompass any subtype of EntityTypes, including the entire union of EntityTypes. While this usually isn't a major issue in practice (as people often specify foo("x") or foo("y") instead of

foo(Math.random()<0.5 ? "x" : "y")
), it can lead to problems with certain example codes like yours.


To address this issue, considering your specific code example, it might be beneficial for GenericEntity to resemble more of a discriminated union with three members rather than a generic type. This can be achieved using a distributive object type as introduced in microsoft/TypeScript#47109. Here's how it looks:

type GenericEntity<T extends EntityTypes = EntityTypes> = { [U in T]: {
  type: U;
  fieldProperty: EntityMappings[U];
} }[T]

This approach involves taking the passed-in type T and mapping over its members, then indexing into it with T. When T is a union, the result will also be a union without any undesired "cross-correlation" terms:

type GE = GenericEntity;
/* type GE = {
    type: "foo";
    fieldProperty: string;
} | {
    type: "bar";
    fieldProperty: number;
} | {
    type: "baz";
    fieldProperty: string[];
} */

(I've also set a default generic parameter for T so that GenericEntity without a type argument aligns with the desired full union.)

Instead of prohibiting unions in T, we're effectively handling them by distributing over them.

Now, the behavior should match your expectations:

const instance: GenericEntity<'foo'> = {
  type: 'foo',
  fieldProperty: 'hello',
} // valid;

const otherInstance: GenericEntity<'baz'> = {
  type: 'baz',
  fieldProperty: ['a', 'b', 'c'],
} // valid

const badInstance: GenericEntity<'foo' | 'baz'> = {
  type: 'baz',
  fieldProperty: 'blah',
}; // error!

Looks promising!

Link to code on TypeScript Playground

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

What is the significance of having nodejs installed in order to run typescript?

What is the reason behind needing Node.js installed before installing TypeScript if we transpile typescript into JavaScript using tsc and run our code in the browser, not locally? ...

Ways to implement a package designed for non-framework usage in Vue

Alert This may not be the appropriate place to pose such inquiries, but I am in need of some guidance. It's more about seeking direction rather than a definitive answer as this question seems quite open-ended. Overview I've created a package th ...

"Ensuring the right data type is selected for the onChange event

In my code, I have a simple select component set up like this. import { Controller } from "react-hook-form"; import Select, { StylesConfig } from "react-select"; //.. const [universe, setUniverse] = useState<SetStateAction<TOptio ...

Should I opt for ionic2 for my production needs?

Currently, I am using Ionic 1 and AngularJS 1 for our applications. We are considering transitioning to Ionic 2 for our new applications. Is this the right decision? I have a few questions: AngularJS 1 and Angular 2 are different, but how does Ionic ...

A step-by-step guide on integrating a basic React NPM component into your application

I've been attempting to incorporate a basic React component into my application (specifically, React-rating). After adding it to my packages.json and installing all the necessary dependencies, I followed the standard instructions for using the compon ...

The magical form component in React using TypeScript with the powerful react-final-form

My goal is to develop a 3-step form using react-final-form with TypeScript in React.js. I found inspiration from codesandbox, but I am encountering an issue with the const static Page. I am struggling to convert it to TypeScript and honestly, I don't ...

Ways to prevent scrolling in Angular 6 when no content is available

I am developing an angular 6 application where I have scrollable divs containing: HTML: <button class="lefty paddle" id="left-button"> PREVIOUS </button> <div class="container"> <div class="inner" style="background:red">< ...

Automatically closing the AppDateTimePicker modal in Vuexy theme after selecting a date

I am currently developing a Vue.js application using the Vuexy theme. One issue I have encountered is with a datetimepicker within a modal. The problem arises when I try to select a date on the datetimepicker component - it closes the modal instead of stay ...

What are some best practices for managing object-level variables in TypeScript and Vue.js?

Uncertain about the optimal approach, I am looking to create a component and leverage some object level variables. Consider the example below: import Vue from "vue" import * as paper from "paper" export default Vue.extend({ template: ` <d ...

The functionality of connect-flash in Express JS is compromised when used in conjunction with express-mysql-session

I am facing a unique issue in my project. I have identified the source of the problem but I am struggling to find a solution. My project utilizes various modules such as cookie-parser, express-mysql-session, express-session, connect-flash, passport and m ...

SVG is not incorporated in the Typescript build

I'm facing an issue where my build using tsc --project tsconfig.dist.json (refer to the file below) does not include the assets (.svg files) that are imported and used in the code. How can I make TypeScript include these assets in the build? Some con ...

Encountered issue with accessing the Error Object in the Error Handling Middleware

Below is the custom error validation code that I have developed : .custom(async (username) => { const user = await UserModel.findOne({ username }) if (user) throw new ConflictError('username already used& ...

Having trouble with Angular router.navigate not functioning properly with route guard while already being on a component?

I am currently troubleshooting an issue with the router.navigate(['']) code that is not redirecting the user to the login component as expected. Instead of navigating to the login component, I find myself stuck on the home component. Upon adding ...

How can I display the top 5 data results after subscribing in Nativescript?

Utilizing this function allows me to retrieve all my data from the web service. public data: Data[]; getall() { this.ws.getalldata().subscribe( data=> { this.data= data; } ); } Here is a ...

Utilizing the useRef hook in React to retrieve elements for seamless page transitions with react-scroll

I've been working on creating a single-page website with a customized navbar using React instead of native JavaScript, similar to the example I found in this reference. My current setup includes a NavBar.tsx and App.tsx. The NavBar.tsx file consists o ...

"Update your Chart.js to version 3.7.1 to eliminate the vertical scale displaying values on the left

Is there a way to disable the scale with additional marks from 0 to 45000 as shown in the screenshot? I've attempted various solutions, including updating chartjs to the latest version, but I'm specifically interested in addressing this issue in ...

Exploring the concept of object inheritance in Angular 5 with Typescript

I'm facing a challenge related to inheritance while building my initial angular 5 application. The error message I encounter is: Property 'message' does not exist on type 'CouponEvent', as reported by the angular-cli. export class ...

Guide to typing a new version of a function without any optional parameters using a mapped tuple

I am attempting to create a modified version of a function that has the same arguments as the original function, but with none being optional. I have tried using a mapped tuple approach with the following logic: type IFArgs = ArgsN<typeof getFunc> t ...

execution of synchronized task does not finish

My approach to running Protractor tests in a headless mode using Xvfb may not be the most efficient, so let me outline my high-level requirement first. I am utilizing the angular2-seed and I aim to execute Protractor tests in a headless mode by incorporat ...

What is the process of transforming two forms into components and then integrating those components into a view in Angular 5?

Currently, I have two forms running smoothly on the same component as shown in InfoAndQualificationComponent.ts import { Component, OnInit } from '@angular/core'; import { FormGroup, FormControl } from "@angular/forms"; @Component({ selector: ...