Building a filter for a union type in TypeScript: a step-by-step guide

Allow me to present an example to demonstrate my current objective.

const v1: { type: "S"; payload: string } = { type: "S", payload: "test" };
const v2: { type: "N"; payload: number } = { type: "N", payload: 123 };

type Actions = typeof v1 | typeof v2;

const findByType = <A extends Actions>(type: A["type"]) => (
    action: Actions
): action is A => action.type === type;

const filterWithBothNameAndType = [v1, v2].filter(findByType<typeof v1>("S"));
console.log(filterWithBothNameAndType[0].payload.trim());

const findByTypeDoesntWork = <A extends Actions, T extends A["type"]>(type: T) => (
    action: Actions
): action is A => action.type === type;

const filterWithJustType = [v1, v2].filter(findByTypeDoesntWork("S"));
console.log(filterWithJustType[0].payload.trim());

typescript playground

I have a function called findByType with correct type information, and another function named filterWithJustType that has an API I prefer, but it loses important type data. My goal is to have the API simply as filter("S") without needing to pass generic types. As of now, it seems like this approach only works with classes and instanceof, yet I aim to make it compatible with plain objects.

Answer №1

To filter out certain values, the Exclude and Extract methods can be used as outlined in the documentation. Here's an example:

type Example1 = Exclude<"apple" | "banana" | "cherry" | "date", "apple" | "cherry" | "fig">;  // "banana" | "date"
type Example2 = Extract<"apple" | "banana" | "cherry" | "date", "apple" | "cherry" | "fig">;  // "apple" | "cherry"

type Example3 = Exclude<string | number | (() => void), Function>;  // string | number
type Example4 = Extract<string | number | (() => void), Function>;  // () => void

Answer №2

Shoutout to @artem for inspiring the creation of ActionMap concept, which led me to introduce actionCreator ensuring synchronization between keys and payload types. Here's a glimpse:

type ActionMap = {
    S: string;
    N: number;
};

function actionCreatorFactory<
    T extends keyof ActionMap,
    P extends ActionMap[T]
>(type: T) {
    return function actionCreator(payload: P) {
        return { type, payload };
    };
}

const actionCreators = {
    s: actionCreatorFactory("S"),
    n: actionCreatorFactory("N"),
};

const v1 = actionCreators.s("test");
const v2 = actionCreators.n(123);

const findByType = <
    T extends keyof ActionMap,
    A extends { type: T; payload: ActionMap[T] }
>(
    type: T
) => (action: A): action is A => action.type === type;

const filterWithJustType = [v1, v2].filter(findByType("S"));
console.log(filterWithJustType[0].payload.trim());

This innovative solution centralizes type declarations in ActionMap, simplifying the process by deriving everything else from it.

UPDATE: Check out the newly published article with additional examples using this methodology https://medium.com/@dhruvrajvanshi/some-tips-on-type-safety-with-redux-98588a85604c

Answer №3

One issue is that the compiler must assume that if a type extending Actions possesses a type member with literal value S, it must be typeof v1. However, this assumption is risky due to the weak constraint of extends Actions, allowing for situations like:

const v3: { type: "S"; payload: boolean } = { type: "S", payload: false };

const filterWithJustType3 = [v1, v2, v3].filter(findByTypeDoesntWork("S"));

While this code does not compile in version 2.6 when strictFunctionTypes is enabled, the additional soundness introduced by this setting is not fully utilized by type inference.

To explicitly inform the compiler that a union member can only be inferred from its type property, you can establish the type mapping manually:

const v1: { type: "S"; payload: string } = { type: "S", payload: "test" };
const v2: { type: "N"; payload: number } = { type: "N", payload: 123 };

interface ActionMap {
    S: typeof v1;
    N: typeof v2;
}

type Actions = ActionMap[keyof ActionMap];


const findByTypeWorks = <T extends keyof ActionMap>(type: T) => (
    action: Actions
): action is ActionMap[T] => action.type === type;

const filterWithJustType = [v1, v2].filter(findByTypeWorks("S"));
console.log(filterWithJustType[0].payload.trim());

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

Ways to set a default value for a union type parameter without encountering the error "could be instantiated with a different subtype of constraint"

After referring to my recent inquiry... Can a default value be specified for valueProp in this scenario? type ValueType = 'value' | 'defaultValue' type Props<T extends ValueType> = Record<T, string> ...

What is the best way to display multiple HTML files using React?

Looking to develop a web application using React that consists of multiple HTML pages. For instance, login.html and index.html have been created and linked to URIs through the backend - resulting in localhost:8080/login and localhost:8080/index. However, R ...

Encountering a Typescript Type error when attempting to include a new custom property 'tab' within the 'Typography' component in a Material UI theme

Currently, I am encountering a Typescript Type error when attempting to add a custom new property called 'tab' inside 'Typography' in my Material UI Theme. The error message states: Property 'tab' does not exist on type &apos ...

Angular 2: The *ngFor directive is unable to locate a suitable differing framework

Below is the code for client.service.ts clients: Client[]; getClientList() { let headers = new Headers(); headers.append('Content-Type', 'application/json'); let authToken = localStorage.getItem('auth_token&apo ...

Angular // binding innerHTML data

I'm having trouble setting up a dynamic table where one of the cells needs to contain a progress bar. I attempted using innerHTML for this, but it's not working as expected. Any suggestions on how to approach this? Here is a snippet from my dash ...

The issue of not displaying the Favicon in Next.js is a common problem

I am currently using Next.js version 13.4.7 with the App directory and I am facing an issue with displaying the favicon. Even though the favicon image is located in the public folder and in jpg format, it is not being displayed on the webpage. However, w ...

Top method for transforming an array into an object

What is the optimal method for transforming the following array using JavaScript: const items = [ { name: "Leon", url: "../poeple" }, { name: "Bmw", url: "../car" } ]; into this object structure: const result = ...

Operators within an observable that perform actions after a specific duration has elapsed

Is there a way in an rxjs observable chain to perform a task with access to the current value of the observable after a specific time interval has elapsed? I'm essentially looking for a functionality akin to the tap operator, but one that triggers onl ...

Encountering difficulty when trying to define the onComplete function in Conf.ts. A type error is occurring, stating that '(passed: any) => void' is not compatible with type '() => void'.ts(2322)'

I have been developing a custom Protractor - browserstack framework from the ground up. While implementing the onComplete function as outlined on the official site in conf.ts - // Code snippet to update test status on BrowserStack based on test assertion ...

Passing data between parent and child components within an Angular application using mat tab navigation

I am currently working on a project, which can be found at this link. Current Progress: I have implemented a mat tab group with tabs inside the app-component. When a tab is clicked, a specific component is loaded. Initially, most of the data is loaded in ...

Preserve the custom hook's return value in the component's state

I am currently facing a challenge in saving a value obtained from a custom hook, which fetches data from the server, into the state of a functional component using useState. This is necessary because I anticipate changes to this value, requiring a rerender ...

The type declaration for the Storage.prototype.setObject method

I'm facing a challenge in creating a d.ts file for the given DOM feature. Storage.prototype.setObject = function(key:string, value:any) { this.setItem(key, JSON.stringify(value)); } Storage.prototype.getObject = function(key:string) { var va ...

Make sure that every component in create-react-app includes an import for react so that it can be properly

Currently, I am working on a TypeScript project based on create-react-app which serves as the foundation for a React component that I plan to release as a standalone package. However, when using this package externally, I need to ensure that import React ...

The functionality of allowEmpty : true in gulp 4.0 does not seem to be effective when dealing with

gulp.task("f1", () => { gulp.src([], {"allowEmpty": true}) .pipe(gulp.dest(location)); }) An error message pops up saying "Invalid glob argument:" when the code above is used. gulp.task("f1", () => { gulp.sr ...

Passing data from getServerSideProps to an external component in Next.js using typescript

In my Index.js page, I am using serverSideProps to fetch consumptions data from a mock JSON file and pass it to a component that utilizes DataGrid to display and allow users to modify the values. export const getServerSideProps: GetServerSideProps = async ...

Transferring 'properties' to child components and selectively displaying them based on conditions

This is the code for my loginButton, which acts as a wrapper for the Button component in another file. 'use client'; import { useRouter } from 'next/navigation'; import { useTransition } from 'react'; interface LoginButtonPr ...

Tips for defining types for specific CSS properties in TypeScript, such as variables

Perhaps there are already solutions out there, and I appreciate it if you can share a link to an existing thread. Nevertheless... In React, when I use the style prop, I receive good autocompletion and validation features like this example: What am I look ...

Eliminating null values from a multidimensional array

Is there a way to remove the array elements cctype, cctypologycode, and amount if they are empty? What would be the most efficient approach? { "ccInput": [ { "designSummaryId": 6, "CCType": "A", "CCTypologyCode": "A", "Amount ...

Dynamically load a custom element with a Component

I am attempting to dynamically inject a component into a container. Component: @Component({...}) export class InvestmentProcess{ @ViewChild('container') container; constructor(public dcl: DynamicComponentLoader) {} loadComponent(fo ...

Thoroughly verifying API responses in RTK Query with Zod schema

I am seeking to verify the API response I receive from a REST API by utilizing a Zod schema. As an illustration, I possess this user schema along with the corresponding API import { z } from 'zod'; import { createApi, fetchBaseQuery } from ' ...