Discover the Category of Union based on Discriminator

Imagine a scenario where there is a concept of a union type called Thing, which combines types Foo, Bar, and Baz, each identified by the property tag.

interface Foo {
  tag: 'Foo'
  foo: string
}

interface Bar {
  tag: 'Bar'
  bar: number
}

interface Baz {
  tag: 'Baz'
  baz: boolean
}

type Union = Foo | Bar | Baz

Now, the goal is to create a mapped type that iterates over the tags within the Union, associating each tag with its corresponding interface type. The main question at hand is: Can we access a type from a union based on its tag value?

interface Tagged {
  tag: string
}

type TypeToFunc<U extends Tagged> = {
  // Is it possible to retrieve the type for the given tag from the union type?
  // What should replace the ??? in this case?
  readonly [T in U['tag']]: (x: ???) => string
}

const typeToFunc: TypeToFunc<Union> = {
  // x must be of type Foo
  Foo: x => `FOO: ${x.foo}`,
  // x must be of type Bar
  Bar: x => `BAR: ${x.bar}`,
  // x must be of type Baz
  Baz: x => `BAZ: ${x.baz}`,
}

If this approach is not feasible, are there other methods to achieve such mapping between types?

Answer №1

Before TypeScript v2.8, there was no direct way to programmatically handle unions. Building unions programmatically in TypeScript was more feasible than trying to inspect them manually. As a workaround, you could create an UnionSchema:

interface UnionSchema {
  Foo: {foo: string},
  Bar: {bar: number},
  Baz: {baz: boolean}
}

type Union<K extends keyof UnionSchema = keyof UnionSchema> = {
  [P in K]: UnionSchema[K] & {tag: K}
}[K]

With this setup, you can refer to individual union constituents as Union<'Foo'>, Union<'Bar'>, and Union<'Baz'>. You can assign names for convenience:

interface Foo extends Union<'Foo'> {}
interface Bar extends Union<'Bar'> {}
interface Baz extends Union<'Baz'> {}

This enables typing your function like so:

type TypeToFunc<U extends Union> = {
  readonly [T in U['tag']]: (x: Union<T>) => string
}
const typeToFunc: TypeToFunc<Union> = {
  // x must be of type Foo
  Foo: x => `FOO: ${x.foo}`,
  // x must be of type Bar
  Bar: x => `BAR: ${x.bar}`,
  // x must be of type Baz
  Baz: x => `BAZ: ${x.baz}`,
}

In TypeScript v2.8 onwards, the introduction of conditional types enhances the expressivity in handling unions. One way to define a union discriminator is by using:

type DiscriminateUnion<T, K extends keyof T, V extends T[K]> = 
  T extends Record<K, V> ? T : never

When combined with the original definitions:

interface Foo {
  tag: 'Foo'
  foo: string
}

interface Bar {
  tag: 'Bar'
  bar: number
}

interface Baz {
  tag: 'Baz'
  baz: boolean
}

type Union = Foo | Bar | Baz

You can now utilize:

type TypeToFunc<U extends Union> = {
  readonly [T in U['tag']]: (x: DiscriminateUnion<Union,'tag',T>) => string
}

This method also works effectively. To try it out immediately, install typescript@next from npm.


I hope this solution proves helpful for your coding endeavors. Best of luck!

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

Encountering an error in Angular 8 where attempting to access an element in ngOnInit results in "Cannot read property 'focus' of null"

My html code in modal-login.component.html includes the following: <input placeholder="Password" id="password" type="password" formControlName="password" class="form-input" #loginFormPassword /> In m ...

What is the solution to fixing the error message "Cannot redeclare block-scoped variable 'ngDevMode' "?

I encountered this error: ERROR in node_modules/@angular/core/src/render3/ng_dev_mode.d.ts(9,11): error TS2451: Cannot redeclare block-scoped variable 'ngDevMode'. src/node_modules/@angular/core/src/render3/ng_dev_mode.d.ts(9,11): error TS2451 ...

What is the reason for the removal of HTML tags in the environment when converting Angular dependencies from es2015 to esm2015 during

After completing the generation of the browser application bundle in Intellij, I noticed that the HTML tags cannot be resolved anymore. What could be causing this issue? I also discovered that if I don't include the AngularMaterialModule in the AppMo ...

Include a control within a form based on the Observable response

I am looking to enhance my form by adding a control of array type, but I need to wait for the return of an Observable before mapping the values and integrating them into the form. The issue with the current code is that it inserts an empty array control e ...

Cannot execute npm packages installed globally on Windows 10 machine

After installing typescript and nodemon on my Windows 10 machine using the typical npm install -g [package-name] command, I encountered a problem. When attempting to run them through the terminal, an application selector window would open prompting me to c ...

Issue with passing parameters to function when calling NodeJS Mocha

I have the following function: export function ensurePathFormat(filePath: string, test = false) { console.log(test); if (!filePath || filePath === '') { if (test) { throw new Error('Invalid or empty path provided'); } ...

Guide on linking action observables to emit values in sync before emitting a final value

If you're familiar with Redux-Observable, I have a method that accepts an observable as input and returns another observable with the following type signature: function (action$: Observable<Action>): Observable<Action>; When this method r ...

Validation issue with Reactive Forms not functioning as expected

My latest project involves a user signup component that I created from scratch import { Component } from '@angular/core'; import {UserManagementService} from '../user-management.service'; import {User} from "../user"; import {FormBuild ...

Passing data through Angular2 router: a comprehensive guide

I am currently developing a web application with the latest version of Angular (Angular v2.0.0). In my app, I have a sub-navigation and I want to pass data to a sub-page that loads its own component through the router-outlet. According to Angular 2 docume ...

typescript error caused by NaN

Apologies for the repetitive question, but I am really struggling to find a solution. I am facing an issue with this calculation. The parameters a to g represent the values of my input from the HTML. I need to use these values to calculate a sum. When I tr ...

What are some solutions to the "t provider not found" error?

Upon deploying my application on the production server using GitLab/Docker, I encountered the following error message: ERROR Error: No provider for t! at C (vendor.32b03a44e7dc21762830.bundle.js:1) at k (vendor.32b03a44e7dc21762830.bundle.js:1) at t._thr ...

Issue at 13th instance: TypeScript encountering a problem while retrieving data within an asynchronous component

CLICK HERE FOR ERROR IMAGE export const Filter = async (): Promise<JSX.Element> => { const { data: categories } = await api.get('/categories/') return ( <div className='w-full h-full relative'> <Containe ...

What could be causing the "ERROR TypeError: Cannot read property 'length' of undefined" message to occur with a defined array in my code?

Even though I defined and initialized my array twice, I am encountering a runtime error: "ERROR TypeError: Cannot read property 'length' of undefined." I have double-checked the definition of the array in my code, but Angular seems to be playing ...

What is the best way to send an enum from a parent component to a child component in

I'm trying to send an enum from a parent component to a child component. This is the enum in question: export enum Status { A = 'A', B = 'B', C = 'C' } Here's the child component code snippet: @Component({ ...

What role does typescript play in this approach?

test.js const testList = [1, 2, 2, 4, 5, 2, 4, 2, 4, 5, 5, 6, 7, 7, 8, 8, 8, 1, 4, 1, 1]; const lastIndex = testList.findLastIndex((e:number) => e === 100); // Property 'findLastIndex' does not exist on type 'number[]'. Did you mean ...

Utilize clipboard functionality in automated tests while using Selenium WebDriver in conjunction with JavaScript

How can I allow clipboard permission popups in automated tests using Selenium web driver, Javascript, and grunt? https://i.stack.imgur.com/rvIag.png The --enable-clipboard and --enable-clipboard-features arguments in the code below do not seem to have an ...

Tips for utilizing the value of object1.property as a property for object2

Within the template of my angular component, I am attempting to accomplish the following: <div> {{object1.some_property.(get value from object2.property and use it here, as it is a property of object1)}} </div> Is there a way to achieve this ...

Using the HTTP Post method to retrieve a file object: a step-by-step guide

Is there a way to utilize a http POST request in order to retrieve a file object? Though the uploading of files to the server using the POST request seems successful and flawless, attempting to fetch the file results in an unusual response: console output ...

Setting up Jest to run in WebStorm for a Vue project with TypeScript can be done through

I am struggling to run all my tests within WebStorm. I set up a project with Babel, TypeScript, and Vue using vue-cli 3.0.0-rc3. My run configuration looks like this: https://i.stack.imgur.com/7H0x3.png Unfortunately, I encountered the following error: ...

'The signatures of each of these values are not compatible with one another.' This error occurs when using find() on a value that has two different array types

Here's the code snippet I'm attempting to run within a TypeScript editor: type ABC = { title: string } type DEF = { name: string } type XYZ = { desc: ABC[] | DEF[] } const container: XYZ = { desc: [{title: & ...