Is there a way to restrict the type of a discriminated union in Typescript without having to list out all the individual cases explicitly?

As I work extensively with discriminated unions, a common issue arises:

When dealing with a function parameter that is a discriminated union type, I often need to perform specific actions based on subsets of the union.

Typically, I use the discriminant to narrow down the type, which works well initially.

The problem occurs when the union expands, resulting in lengthy

if (x.type === 'a' || x.type === 'b' || x.type === 'c' || ...)
chains.

In JavaScript, I'd handle this by using

if (['a','b','c'].includes(x.type))
, but TypeScript doesn't support this approach.

Consider the following example:

type Certificate = {
    type: 'NATIONAL_ID',
    nationality: string
} | {
    type: 'PASSPORT',
    passportNumber: string
} | {
    type : 'INSURANCE_CARD',
    provider: string
} | {
    type: 'BIRTH_CERTIFICATE',
    date: string
}| {
    type: 'DEATH_CERTIFICATE',
    date: string
}| {
    type: 'MARRIAGE_CERTIFICATE',
    date: string
}

// While functional, this method requires listing out each discriminant case 
const goodPrintDate = (certificate: Certificate) => {
    if (certificate.type === 'BIRTH_CERTIFICATE' || certificate.type === 'DEATH_CERTIFICATE' || certificate.type === 'MARRIAGE_CERTIFICATE') {
        console.log(certificate.date)
        return
    }

    console.log(`The certificate of type ${certificate.type} does not have a date`)
}

// Unfortunately, this attempt does not work as expected
const badPrintDate = (certificate: Certificate) => {
    const certificateTypesWithDate = ['BIRTH_CERTIFICATE', 'DEATH_CERTIFICATE', 'MARRIAGE_CERTIFICATE']

    if (certificateTypesWithDate.includes(certificate.type)) {
        // The code only reaches here for specified types, but TS fails to infer it
        console.log(certificate.date) 
        return
    }
    
    console.log(`The certificate of type ${certificate.type} does not have a date`)
}

Is there a more efficient way to structure goodPrintDate without repeatedly enumerating every time? Perhaps by moving this logic to a separate function like (isCertificateWithDate / isCertificateTypeWithDate).

I've experimented with various solutions such as using sets instead of arrays, but nothing seems to be effective.

Answer №1

To create a personalized type prediction function, you can follow these steps:

// Developing a unique type prediction function
function isDocumentWithType(document: Document): document is PASSPORT | DRIVER_LICENSE | RESIDENCE_PERMIT {
    return ['PASSPORT', 'DRIVER_LICENSE', 'RESIDENCE_PERMIT'].includes(document.type)
}

const validateExpiryDate = (document: Document) => {
    if (isDocumentWithType(document)) {
        console.log(document.expiryDate) // Success
        //          ^? parameter document: PASSPORT | DRIVER_LICENSE | RESIDENCE_PERMIT
        return
    }

    console.log(`The document of type ${document.type} does not have an expiry date`)
    //                                 ^? parameter document: ID_CARD | SOCIAL_SECURITY_CARD | VISA
}

For a more adaptable alternative, consider using a generic type:

// Customized flexible type prediction function
function isDocumentOf<T extends Document['type']>(document: Document, ...types: T[]): document is Document & { type: T } {
    return types.includes(document.type as any) // document type may not necessarily be in T
}

const validateExpiryDate2 = (document: Document) => {
    if (isDocumentOf(document, "PASSPORT", 'DRIVER_LICENSE')) {
        console.log(document.expiryDate) // Successful
        //          ^? (parameter) certificate: PASSPORT | DRIVER_LICENSE
        return
    }

    console.log(`The document of type ${document.type} lacks an expiry date.`)
    //                                 ^? parameter document: ID_CARD | SOCIAL_SECURITY_CARD | WORK_PERMIT | RESIDENCE_PERMIT
}

View the Playground Link for demonstration.

Answer №2

If you're looking for a solution to your issue, one option could be utilizing the switch statement with fall-through:

const printDate = (certificate: Certificate) => {
    switch (certificate.type) {
        case 'BIRTH_CERTIFICATE':
        case 'DEATH_CERTIFICATE':
        case 'MARRIAGE_CERTIFICATE':
            console.log(certificate.date) 
            return
        default:
            console.log(`certificate of type ${certificate.type} has no date`)
    }
}

This approach neatly presents all relevant cases in a clear manner. For further exploration of narrowing down types, I suggest taking a look at https://www.typescriptlang.org/docs/handbook/2/narrowing.html.

The discussion on whether includes should be used for type narrowing has previously taken place, as seen in https://github.com/microsoft/TypeScript/issues/36275. Ultimately, the TypeScript team opted against it due to the complexity outweighing the benefits. It may be beneficial to explore this further for a deeper comprehension of the issue.

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

Bundling and minifying Angular2 assets

In the world of ASP.NET (or gulp), bundling and minification are taken care of. However, a different issue arises when following Angular2 tutorials: the view HTML is typically embedded within the component itself. Fortunately, there is a way to separate th ...

A different approach to fixing the error "Uncaught (in promise) TypeError: fs.writeFile is not a function" in TensorFlow.js when running on Chrome

I've been attempting to export a variable in the TensorFlow posenet model while it's running in the Chrome browser using the code snippet below. After going through various discussions, I discovered that exporting a variable with fswritefile in t ...

When running the command ng build --prod, it seems that the module for class X cannot be determined. To resolve this issue, make sure to include

I'm encountering an issue while trying to develop my Angular 5 application. The error message reads: Cannot determine the module for class ThreadListTabsComponent in /home/brightwater/Differ/src/app/thread-lists/thread-lists.component.ts! Add T ...

Angular 2 interprets my JSON object as a function

My webapp retrieves data via Json and places it in objects for display. I have successfully implemented this process twice using services and classes. However, when I recently copied and pasted the old code with some modifications to redirect to the correc ...

The state is accurate despite receiving null as the return value

I'm feeling a bit lost here. I have a main component that is subscribing to and fetching data (I'm using the redux dev tools to monitor it and it's updating the state as needed). In component A, I am doing: public FDC$ = this.store.pipe(sel ...

One way to update the value of the current array or object using ngModel in Angular 2 is to directly

I have a situation where I am dealing with both an array and an object. The array is populated with data retrieved from a service, while the object contains the first element of that array. feesEntries: Array<any> = []; selectedFeesEntry: any; clien ...

Exploring TypeScript's Classes and Generics

class Person { constructor(public name: string) {} } class Manager extends Person {} class Admin extends Person {} class School { constructor(public name: string) {} } function doOperation<T extends Person>(person: T): T { return person; } ...

What is preventing me from running UNIT Tests in VSCode when I have both 2 windows and 2 different projects open simultaneously?

I have taken on a new project that involves working with existing unit tests. While I recently completed a course on Angular testing, I am still struggling to make the tests run smoothly. To aid in my task, I created a project filled with basic examples f ...

RTK update mutation: updating data efficiently without the need to refresh the page

I am facing an issue with my mui rating component in a post-rating scenario. Although the rating updates successfully in the data, the page does not refresh after a click event, and hence, the rating remains enabled. To address this, I have implemented a d ...

Expanding a JSON data structure into a list of items

In my Angular service script, I am fetching customer data from a MongoDB database using Django: getConsumersMongodb(): Observable<any> { return this.httpClient.get(`${this.baseMongodbApiUrl}`); } The returned dictionary looks like this: { &q ...

Troubleshooting: JavaScript code not functioning properly with variable input instead of fixed value

I have encountered an issue with a JS function that I'm using. The function is shown below: // A simple array where we keep track of things that are filed. filed = []; function fileIt(thing) { // Dynamically call the file method of whatever ' ...

Angular 2 Validation Customizer

Recently, I created a web API function that takes a username input from a text field and checks if it is already taken. The server response returns Y if the username is available and N if it's not. For validating the username, I implemented a Validat ...

GraphQL queries that are strongly-typed

Currently working on a Vue CLI project where I am utilizing axios as my request library. In all the examples I've come across, they use strings for queries like this: { hero { name friends { name } } } Given that I am employing ...

What is the process for removing globally declared types in TypeScript definitions?

There are numerous libraries that cater to various platforms such as the web, Node servers, and mobile apps like React Native. This means they can be integrated using <script /> tags, require() calls, or the modern import keyword, particularly with t ...

Exploring Angular Testing with SpyOn

Apologies for my inexperience with Angular, but I am struggling with using spyOn in a unit test. In my unit test, there is a method on the component that calls service1, which in turn calls another service2. However, when I try to spyOn service1 in order ...

Saving JSON data retrieved from the server into an array in Angular 2

Using a nodejs server to retrieve data from an SQL database has been challenging. I attempted to store the data in taches, which is an array of Tache : getTaches(): Observable<Tache[]> { return this.http.get(this.tachesUrl) .map(response => ...

Removing an object from an array when a certain key value already exists in TypeScript

I'm currently facing an issue with my function that adds objects to an array. The problem arises when a key value already exists in the array - it still gets added again, but I want it to only add if it doesn't exist yet. Here's what I have: ...

ngx-slick-carousel: a carousel that loops infinitely and adjusts responsively

I have implemented ngx-slick-carousel to showcase YouTube videos in my project. However, I am facing two main issues. Firstly, the carousel is not infinite, and when the last video is reached, there appears to be white spaces before it loops back to the fi ...

The resolver function in the Nextjs higher order API is not defined

I am trying to create a custom wrapper function for my NextJs API routes that will verify a JWT in the request, validate it, and then execute the original API handler. Here is how I have defined my wrapper function: interface ApiError { message: string, ...

Encountering an error: "Unable to assign the 'id' property to an undefined object while attempting to retrieve it"

I'm running into an issue while attempting to retrieve a specific user from Firebase's Firestore. export class TaskService { tasksCollection: AngularFirestoreCollection<Task>; taskDoc: AngularFirestoreDocument<Task>; tasks: Obs ...