Filter the union type to include only members that have a specific field

I am working with a discriminated union type that includes a non-discriminant field in multiple members:

interface Foo {
  type: 'foo';
  foo: string;
}

interface OptionalFoo {
  type: 'optionalfoo';
  foo?: string;
}

interface RequiredNullableFoo {
  type: 'requirednullable';
  foo: string | null | undefined;
}

interface Bar {
  type: 'bar';
  bar: string;
}

type All = Foo | OptionalFoo | RequiredNullableFoo | Bar;

I want to create a new type using a type definition:

type Foos = UnionMembersWithField<All, 'foo'>;
// Foos = Foo | OptionalFoo | RequiredNullableFoo;

I have tried various ways to define UnionMembersWithField, but none of them are completely successful:

// Utility type: `keyof All` is `'type'`, because it is the only field in common,
// but we want to accept any field from any union type.
type KeysOfUnion<T> = T extends T ? keyof T : never;

// This definition yields `Foo | RequiredNullableFoo` and drops `OptionalFoo`.
type UnionMembersWithField<T, K extends KeysOfUnion<T>> = T extends Record<K, any> ? T : never;

// This definition does not compile due to TS1170, but seems like the best expression of intent.
type UnionMembersWithField<T, K extends KeysOfUnion<T>> = T extends { [K]?: any } ? T : never;

// This definition yields `never`...
type UnionMembersWithField<T, K extends KeysOfUnion<T>> = Required<T> extends Record<K, any> ? T : never;

// ...which is surprising, because redefining it with a helper `DiscriminateUnion` type yields
// `Required<Foo> | Required<OptionalFoo> | Required<RequiredNullableFoo>`
type DiscriminateUnion<T, K extends keyof T, V extends T[K]> = T extends Record<K, V> ? T : never;
type UnionMembersWithField<T, K extends KeysOfUnion<T>> = DiscriminateUnion<Required<T>, K, any>;

My other attempts either had no impact (resulting in the same output type as the input type) or resulted in never.

Considering that All['foo'] is any (because Typescript doesn't provide a clear type when not all union members have the specified field), I am unsure if such a type is achievable.

Answer №1

Here's a solution that should work:

type UnionMembersWithField<U, F> = 
  U extends U           // using conditional to distribute U
    ? F extends keyof U // checking if the field F is in keyof U
      ? U
      : never
    : never

type Foos = UnionMembersWithField<All, 'foo'>;
// Result: Foos = Foo | OptionalFoo | RequiredNullableFoo;

Try it out on Playground

Answer №2

Here is a solution you can utilize:

type KeysOfUnion<T> = T extends T ? keyof T : never;
type UnionMembersWithField<T, K extends KeysOfUnion<T>> = Exclude<T, Required<Exclude<T, Pick<Required<T>, K>>>>

Let's delve deeper into how this works. To begin with, consider the following:

type UnionMembersWithField<T, K extends KeysOfUnion<T>> = Exclude<T,Exclude<T, Pick<T, K>>>
:

  • We are extracting the keys from each type
  • and then removing those types from the union
  • this will give us the opposite of what we need
  • so we remove them one more time

This approach does not account for optional types, hence we introduce Required to obtain the finalized version.

You can experiment further using this playground.

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

Steer clear of using enum values in typescript to prevent potential issues

I am looking to iterate through an enum type in order to populate options within a react component. Below, you will find the specific enum and a function that retrieves its keys and values. export enum TaskType { daily, weekly, monthly, yearly } ...

Encountering a clash in Npm dependencies

I have been involved in a Vue project where I utilized Vue Cli and integrated the Typescript plugin. However, I encountered multiple vulnerabilities that need to be addressed. When I executed npm audit fix, it failed to resolve the dependency conflict: npm ...

The cause of Interface A improperly extending Interface B errors in Typescript

Why does extending an interface by adding more properties make it non-assignable to a function accepting the base interface type? Shouldn't the overriding interface always have the properties that the function expects from the Base interface type? Th ...

Different TypeScript parameters that cannot be used together

Consider the given JavaScript function below: function x({foo, fooId, bar, barId}) {} I am looking to refactor this function into TypeScript in such a way that the caller is required to provide either foo or fooId, but not both. The same rule should apply ...

Encountering an error message stating "Buffer is not defined" while working with gray-matter

Encountering an issue when trying to utilize gray-matter in Angular 9, the error message displayed is: ReferenceError: Buffer is not defined at Object.push../node_modules/gray-matter/lib/utils.js.exports.toBuffer (utils.js:32) at push../node_modul ...

A Guide to Filtering MongoDB Data Using Array Values

I am trying to extract specific data from a document in my collection that contains values stored in an array. { "name": "ABC", "details": [ {"color": "red", "price": 20000}, {" ...

TSDX incorporates Rollup under the hood to bundle CSS Modules, even though they are not referenced

I have recently developed a React library with TSDX and you can find it here: https://github.com/deadcoder0904/react-typical This library utilizes CSS Modules and applies styles to the React components. Although the bundle successfully generates a CSS fi ...

Is there a more effective method to return a response apart from using a redundant function?

function unnecessaryFunction(){ let details: SignInDetails = { user: user, account: account, company: company }; return details; } I am being told that the details value is unnecessary. Is there ...

How to execute a function in a child component that is declared in the parent component using Angular

Is anyone able to help me with an issue I am facing in my Angular project? I have two components, 'app' and 'child'. Within the child component, I have a button that calls a function defined in the app component. However, this setup is ...

What is the best way to retrieve the global styles.scss file from the assets folder for privacy, legal, and conditions pages

I have a static page called terms.html located at: $PROJECT_ROOT/src/assets/html/terms.html In addition, my global styles (used by all components) are found at: $PROJECT_ROOT/src/styles.scss To include the static html file in a component, I use the fol ...

Converting text files into JSON format

I am working with a text file that has a specific structure. Title: Tombstone Release Year: 1993 Format: Blu-ray Stars: Kurt Russell, Val Kilmer, Sam Elliott, Bill Paxton Title: The Shawshank Redemption Release Year: 1994 Format: DVD Stars: Tim Robbins, M ...

Is it possible to pass a different variable during the mouse down event when using Konva for 2D drawing?

I am trying to pass an additional value in a mouse event because my handleMouseDown function is located in another file. stage.on('mousedown', handleMouseDown(evt, stage)) Unfortunately, I encountered an error: - Argument of type 'void&apos ...

Mapping values of 2 objects in TypeScript through an interface

I am currently working with two objects, objA and objB. There is a function that I have, which takes a value from objB and a key from objA as arguments. const objA = { a:"a", b:"b" } const objB = { a:"a", b:"b&qu ...

Scoped variable in Typescript producing a generated Javascript file

I'm currently learning TypeScript through an online course, and I've encountered a problem that seems to be related to a VSCode setting. Whenever I compile app.ts, it generates the app.js file, but I immediately encounter a TypeScript error. It& ...

Tips for effectively utilizing dragstart, dragend, click, mouseup, and mousedown events simultaneously with three separate div elements, ensuring each maintains its distinctiveness and equality

Alright, I'm trying to make some progress: This is the issue at hand: Take a look at my HTML: <!-- PARENT OUTER WRAPPER --> <div id="avatarmoveable" class="moveablecontainer avatarBubble" title="Click on an HOLD to drag avatar" ( ...

Tips for using map on an array to create and return an object with the help of tslint and its syntactic sugar

This is a straightforward question about code style. How do I use the map function to iterate over an array and return a new object without triggering TSLint warnings? TSLint suggests simplifying the arrow function by removing curly braces, 'retur ...

Troubleshooting a NextJS Middleware Issue: Retrieving a Blank Screen After Token Verification

I'm currently developing a Spotify Clone using its API and utilizing Next JS Middleware to enforce redirect conditions. The purpose of these conditions is to verify if the user has a token; if not, they should be redirected to the login page. For som ...

Guide to executing various datasets as parameters for test cases in Cypress using Typescript

I am encountering an issue while trying to run a testcase with multiple data fixtures constructed using an object array in Cypress. The error message states: TS2345: Argument of type '(fixture: { name: string; sValue: string; eValue: string}) => vo ...

What methods can I use to integrate a Google HeatMap into the GoogleMap object in the Angular AGM library?

I am trying to fetch the googleMap object in agm and utilize it to create a HeatMapLayer in my project. However, the following code is not functioning as expected: declare var google: any; @Directive({ selector: 'my-comp', }) export class MyC ...

Ensure to provide a promise within the forEach loop

How do I go about returning a promise in the deleteBudgets() method below so that I can use await inside the updateBudgets() method? deleteBudgets deleteBudgets(data: Budget[], projectId: string): Promise<void> { forEach(data, (d: Budget) =&g ...