Is it possible to maintain type inference in union types when utilizing type guards within a function?

Imagine I am working with three unique types (and one union type)

type Alpha = {
    type: 'a'
    title: string
    description: string
}

type Beta = {
    type: 'b'
    title: string
}

type Charlie = {
    type: 'c'
    description: string
}

type Delta = Alpha | Beta | Charlie

I understand that proper type inference can be achieved using the === operator

function monitor(t: Delta) {
    if (t.type === 'a' || t.type === 'b') console.log(t.title) // no issues here
    if (t.type === 'a' || t.type === 'c') console.log(t.description) // no issues here
}

But, is it feasible to create a utility function like this:

function matches<T extends { type: string }>(t: T, types: T['type'][]) : boolean {
    return types.includes(t.type)
}

So that I can perform the following without any errors?

function monitor(t: Delta) {
    if (matches(t, ['a', 'b'])) console.log(t.title) // Property 'title' does not exist on type Delta
    if (matches(t, ['a', 'c'])) console.log(t.description) // Property 'description' does not exist on type Delta
}

Answer №1

To specify the behavior of matches(), you can define it as a custom type guard function. This helps the compiler understand that the output of the function, either true or false, narrows down the type of the parameter t.

One approach to achieving this is:

function matches<T extends { type: string }, K extends string>(
    t: T, types: K[]
): t is Extract<T, { type: K }> {
    const widenedTypes: readonly string[] = types;
    return widenedTypes.includes(t.type);
}

This implementation makes use of generics for both T, which represents the type of t, and K, which denotes the union of string literal types in types. The return type is defined as Extract<T, {type: K}, utilizing the Extract<T, U> utility type to filter out elements in type T that align with {type: K}.

It's important to note that there might be compiler complaints regarding types.includes(t.type) due to the narrower nature of elements in

types</code compared to <code>t.type
. One workaround is to widen types from K[] to readonly string[] before invoking includes(). Refer to this question for more insights.


Let's test if the function works as expected:

function logger(t: D) {
    if (matches(t, ['a', 'b'])) console.log(t.title) // okay
    if (matches(t, ['a', 'c'])) console.log(t.description) // okay
}

Results look promising!

Link to Playground with code snippet

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

Verifying the implementation of IEnumerable for a Type or instance without considering the specific Type T

In my current project, I am deeply reflecting on how to keep things organized and tidy by providing helper methods. My goal is to create two methods that can determine if a type or instance implements IEnumerable, regardless of the specific type T. Here i ...

Are there any events in Angular maps like ZoomShift or MoveToCenter?

Greetings, I am currently involved in a project that utilizes Angular Maps, with Angular 6 for the frontend and Python for the backend. My task at hand is to retrieve the coordinates of certain points within a defined range of kilometers from the API. The ...

Sorry, I cannot complete this task as it involves rewriting copyrighted content

I recently implemented the useRef hook in my scroll function, specifying HTMLDivElement as the type. However, I encountered an issue where I received the error message "Property 'clientHeight, scrollHeight, scrollTop' does not exist on type &apos ...

Enable the validation of properties that are not explicitly defined when using the @ValidateNested

I am using NesteJs and I am looking for a way to instruct @ValidateNested to skip properties that are not defined in the class without triggering an error: property should not exists Here are my classes: export default class InitialConfigClass extends Sha ...

Unable to iterate through elements when utilizing an external library: Incompatible types, 'Element[]' cannot be assigned to type 'string'

I've encountered an issue while trying to use the react-responsive carousel. It seems to work fine when I manually add my images, but when I try to add them through photos.map, it throws an error: Type 'Element[]' is not assignable to type ...

TypeScript maintains the reference and preserves the equality of two objects

Retrieve the last element of an array, make changes to the object that received the value, but inadvertently modify the original last position as well, resulting in both objects being identical. const lunchVisit = plannedVisits[plannedVisits.length ...

The Ant Design Table is reminding us that every child in a list must be assigned a unique "key" prop

I've successfully integrated a real-time data grid table using React Redux and SignalR. However, upon adding the first item to the table or dispatching for the first time, I encounter the following warning in the console: Warning: Each child in a li ...

Can a unique intrinsic type be created from scratch?

Ever since template literals were introduced in Typescript (PR), we've had access to various useful functions in our types: Uppercase Lowercase Capitalize Uncapitalize For more information, refer to the official documentation. Although it may seem ...

Before the service call finishes, Akita queries this.selectAll and returns an empty list

Here is the code snippet from my file named widgetquery.ts: @Injectable({ providedIn: 'root' }) export class WidgetQuery extends QueryEntity<WidgetState, WidgetTO> { public Widget$: Observable<WidgetTO> = this.selectActive().filter( ...

Instances of classes in Typescript modules not properly imported resulting in undefined values

I am dealing with two files app.js ///<reference path='mongodb.d.ts'/> ///<reference path='MyDatabase.ts'/> module MyModule { import mongodb = module("mongodb"); new mongodb.Server(); var db = new MyDatabase(); // Th ...

Encountering issues while trying to run npm install for an Angular 7 application, specifically receiving an error stating: "Module not found: @angular-devkit/build-ng-packagr." This error is hindering

I don't have much experience with JavaScript, node, npm, Angular, etc. My expertise lies in TypeScript as I am still a beginner. However, I recently inherited an application that requires maintenance to resolve a cross-site cookie issue. As I attempt ...

Is there a way to modify the format received from the backend Angular REST API?

Below is the code I am currently using: this.elService.getOne(id) .subscribe((result: ModelName) => { let test = this.datePipe.transform(result['birthDate'], 'mm/dd/yyyy') result['birthDate']= test console.log(result ...

Implementing click binding on elements generated post applyBindings operation

After calling applyBindings, I have an element that is dynamically created. <span data-bind="html: $root.someObservable() && $root.generateLink()" /> The someObservable observable is set to true after applyBindings is called. The generateLi ...

When attempting to run the yarn build dist command, an error of type TypeError is encountered, stating that it is not possible to set the constructor property

Upon executing the following command in a GitHub workflow, I encountered this error: npm-run-all -p types transpile @internal_package: $ swc src --out-dir dist @internal_package: /home/runner/work/repo-name/repo-name/node_modules/ttypescript/lib/loadTypesc ...

Simulate a lower resolution device using the Chrome developer tool

I'm looking to test the Ionic 3 app on a low-resolution device, but I don't have one on hand. Can anyone guide me on how to simulate this in the Chrome Dev Tools? Low-Resolution Device: Zte Screen Size: 5.0 Inches, 480 x 854 pixels https://i ...

Issues with command functionality within the VS Code integrated terminal (Bash) causing disruptions

When using Visual Studio Code's integrated terminal with bash as the shell, I have noticed that commands like ng and tsc are not recognized. Can anyone shed some light on why this might be happening? ...

When using @testing-library/react (rtl), the 'waitFor' function achieves success even without the need for the 'await' keyword

waitFor() is causing my test to fail while waitFor() (without await) makes it pass. The official documentation states: Async methods return a Promise, so you must always use await or .then(done) when calling them. (https://testing-library.com/docs/guide ...

What are the most optimal configurations for tsconfig.json in conjunction with node.js modules?

Presently, I have 2 files located in "./src": index.ts and setConfig.ts. Both of these files import 'fs' and 'path' as follows: const fs = require('fs'); const path = require('path'); ...and this is causing TypeScr ...

TypeScript: Defining an Array Type within a Namespace or Module

Within a specific namespace, I have the following code: const operation1 = Symbol("operation1"); const operation2 = Symbol("operation2"); export interface Array<T> extends IConjable<T>, ISeqable<T> {} Array.prototype[op ...

Achieving Type-Safety and Flexibility in React HOCs: Ensuring Strong Typing While Allowing for Any Additional

It's a common scenario in React to use a Higher Order Component (HOC) where certain props are required, but we also want the consumer of the HOC to be able to pass any additional props they desire. Achieving this is straightforward with the use of des ...