Improved ergonomics for enhancing TypeScript union-narrowing typeguard function

Within our codebase, we have a utility that generates a typeguard to narrow down a discriminated union:

export type ExtractBranchFromUnion<
  UNION,
  DISCRIMINANT extends keyof UNION,
  BRANCH extends UNION[DISCRIMINANT],
> = UNION extends Record<DISCRIMINANT, BRANCH> ? UNION : never;

export function narrow<
  UNION,
  DISCRIMINANT extends keyof UNION,
  BRANCH extends UNION[DISCRIMINANT],
>(
  discriminate: DISCRIMINANT,
  branch: BRANCH,
): (
  item: UNION,
) => item is ExtractBranchFromUnion<UNION, DISCRIMINANT, BRANCH> {
  return (item): item is ExtractBranchFromUnion<UNION, DISCRIMINANT, BRANCH> =>
    item[discriminate] === branch;
}

It can easily be utilized in a filter function to narrow down an array to a specific union member. However, it is important to remember adding as const after a string literal branch argument to prevent incorrect type inference:

type A = {type: 'A'; a: string};
type B = {type: 'B'; b: string};
type SimpleUnion = A | B;
const arr: SimpleUnion[] = [];

// Incorrect: type is SimpleUnion[]
const badListOfBs = arr.filter(narrow('type', 'B'));

// Correct: type is B[]
const goodListOfBs = arr.filter(narrow('type', 'B' as const));

Although using pre-made constants like

const Types = {
  'A': 'A',
  'B': 'B',
} as const;

// Works fine: type is B[] but requires a pre-made constant
const okayListOfBs = arr.filter(narrow('type', Types.B));

helps mitigate the issue, there is a risk of forgetting as const with a literal value and causing confusion. Additionally, the as const syntax may not be visually appealing in code. Is there a way to enhance TypeScript to automatically infer a narrower type when a string literal is provided in narrow? Alternatively, could an informative error message be triggered?

Access the playground with all the code mentioned above.

Answer №1

To further restrict the Branch generic, you can add an additional constraint:

export type ExtractBranchFromUnion<
  Union,
  Discriminant extends keyof Union,
  Branch extends Union[Discriminant] & PropertyKey,
  > = Union extends Record<Discriminant, Branch> ? Union : never;

export function narrow<
  Union,
  Discriminant extends keyof Union,
  Branch extends Union[Discriminant] & PropertyKey, 
  >(
    discriminate: Discriminant,
    Branch: Branch,
): (
    item: Union,
  ) => item is ExtractBranchFromUnion<Union, Discriminant, Branch> {
  return (item): item is ExtractBranchFromUnion<Union, Discriminant, Branch> =>
    item[discriminate] === Branch;
}



type A = { type: 'A'; a: string };
type B = { type: 'B'; b: string };

type SimpleUnion = A | B;
const arr: SimpleUnion[] = [];

const badListOfBs = arr.filter(narrow('type', 'B')); // B[]
const goodListOfBs = arr.filter(narrow('type', 'A')); // A[]

Interactive Example

An extra constraint was introduced with

Branch extends Union[Discriminant] & PropertyKey
.

It is likely that the discriminator value should be limited to string | number | symbol

Note: Generic names have been capitalized for readability, but this can be a matter of personal preference.

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

Develop a library of components using TypeScript and Less files

I'm currently in the process of creating a React UI library that will consist of various components such as Buttons, Inputs, textareas, etc. This library, which I've temporarily named mylib, will be reused across multiple projects including one c ...

TypeScript asserts that the Function is not callable

There seems to be an issue with TypeScript not recognizing that a function of type Function is not callable. type Constructable = { new(...args: any[]): any } function isClass(func: any) { return ( typeof func === 'function' && ...

Unit Testing Sentry with Jest Framework using TypeScript in a Node.js Environment

I'm in the process of integrating Sentry into my existing NodeJS project, but I'm facing an issue with mocking a specific part of the Sentry code. Here's the relevant Sentry code snippet: const app: express.Express = express(); Sentry.init ...

Creating an observer for a multiple selection dropdown in Aurelia: step by step tutorial

In my current setup, I have a dropdown menu that allows users to select a single option. This selection automatically provides me with the filtering value as an observable. Here is how it works: public months: any=[]; @observable public selectedMonth: ...

Arranging Alphanumeric Characters in Angular in Ascending Order

I am trying to sort a list of characters first, followed by alphanumeric values. Here is what I have: [Austria , Germany , 123aed , 234eds] This is my current sorting attempt: obj.sort((a,b) => { if ( (isNaN(a.text) && isNaN(b.text)) || ...

Utilizing AWS CDK to Define StackProps Input Variables

Recently, I have started using the AWS CDK and encountered a challenge. I want to allow end users to define custom input variables when using my AWS CDK without having to edit the entire code. While I have been able to work with standard types such as stri ...

The Battle of Identifiers: Named Functions against Anonymous Functions in TypeScript

When it comes to performance and performance alone, which option is superior? 1) function GameLoop() { // Performing complex calculations requestAnimationFrame(GameLoop); } requestAnimationFrame(GameLoop); 2) function GameLoop() { // ...

Error involving key mismatch between TypeScript inherited interface and literal string type

There are 3 interfaces (A, B, and C) that all extend from a shared interface (Common). Additionally, there is a container type which holds arrays of these 3 interfaces (Container). The goal is to select one of the arrays and extract a common property from ...

Encountering Next.js Hydration Issue when Using Shadcn Dialog Component

While working on a Next.js project, I came across a hydration error when utilizing the Shadcn Dialog component. The specific error message reads: "Hydration failed because the initial UI does not match what was rendered on the server." Highligh ...

Distribute your SolidJS Typescript components on npm

Recently, I developed a SolidJS TypeScript UI component and successfully published it to the npm registry. The project structure is organized as follows: |-app |-index.html |-src |-index.tsx |-App.tsx |-utils |- ... |-com ...

Sorting the material table based on the column IDs, which usually correspond to the column names, may not align with the properties of the data

.ts this.displayedColumns = [ { key: 'id', header: '#' }, { key: 'fullname', header: 'Full name' }, { key: 'email', header: 'email' }, { key: 'roleName', header: ...

Is there a function in Zod similar to Yup's oneOf()?

If I wanted to restrict a property to specific values using Yup, it could be achieved with the code snippet below: prop: Yup.string().oneOf([5, 10, 15]) However, I haven't found a similar method in Zod. Nonetheless, I can still validate it by: const ...

The implementation of user context failed to meet expectations in terms of writing

I need some clarification regarding userContext in react with typescript. Initially, I define it in RubroContext.tsx import { createContext, useContext } from "react"; import { RubroType1, RubroType2 } from "../Interfaces/interfaces"; ...

I'm having trouble inputting text into my applications using React.js and TypeScript

I am encountering an issue where I am unable to enter text in the input fields even though my code seems correct. Can anyone help me figure out what might be causing this problem? Below is the code snippet that I am referring to: const Login: SFC<LoginP ...

understanding the life cycle of components in Ionic

I created a component with the following structure: export class AcknowledgementComponent implements AfterViewInit { private description: string; @Input('period') period: string; constructor() { } ngAfterViewInit() { console.log ...

Receiving the error "Potential null object. TS2531" while working with a form input field

I am currently working on developing a straightforward form to collect email and password details from new users signing up on Firebase. I am utilizing React with Typescript, and encountering an error labeled "Object is possibly 'null'. TS2531" s ...

Changing properties of a parent component from a child component in Angular 2

Currently, I am utilizing the @input feature to obtain a property from the parent component. This property is used to activate a CSS class within one of the child components. Although I am successful in receiving the property and activating the class init ...

When utilizing typescript to develop a node module and importing it as a dependency, an issue may arise with a Duplicate identifier error (TS2300)

After creating a project called data_model with essential classes, I built a comprehensive gulpfile.js. This file not only compiles .ts to .js but also generates a unified .d.ts file named data_model.d.ts, which exports symbols and is placed at the root of ...

Allowing cross-origin resource sharing (CORS) in .NET Core Web API and Angular 6

Currently, I am facing an issue with my HTTP POST request from Angular 6. The request is successfully hitting the .net core Web API endpoint, but unfortunately, I am not receiving the expected response back in Angular 6. To make matters worse, when checkin ...

Why does Angular throw a NullReferenceException when calling User.FindFirst(ClaimTypes.NameIdentifier), whereas Postman executes the same code without any

I'm currently troubleshooting a NullReferenceException in a .NET Core API and Angular application, but I've hit a roadblock. The specific issue arises when trying to update the "About" section of a User. Take a look at the text area screenshot ...