Identify the Type of a Field in a Typescript Union

I am facing an issue with a union type in my code:

type Option1 = {
    items: string[];
}
type Option2 = {
    delete: true;
}
type Combined = Option1 | Option2;

My goal is to create a new variable that has the same type as the items field:

const items_variable:Combined["items"] = ["a", "b"];

However, I encounter the error message

error TS2339: Property 'items' does not exist on type 'Combined'
.

If I were working with values instead of types, I could use type narrowing, but I'm unsure how to apply it to an existing type.

It seems like everything would work fine if Option2 had the items field, but that doesn't align with my project requirements.

Any suggestions on what type I should assign to items_variable?

Answer №1

TS unions operate in a specific manner that is intentional.

type Option1 = {
    items: string[];
}
type Option2 = {
    delete: true;
}
type Combined = Option1 | Option2;

type Keys = keyof Combined; // never

The result of keyof Combined is never, representing an empty set of keys.

This occurs because Option1 and Option2 do not share any common properties, making it difficult for TS to determine which property is permitted.

If you have a function that accepts either Option1 or Option2, you should utilize custom typeguards when working with Combined:

type Option1 = {
    items: string[];
}
type Option2 = {
    delete: true;
}
type Combined = Option1 | Option2;

type Keys = keyof Combined; // never

const hasProperty = <Obj, Prop extends string>(obj: Obj, prop: Prop)
    : obj is Obj & Record<Prop, unknown> =>
    Object.prototype.hasOwnProperty.call(obj, prop);

const handle = (union: Combined) => {
    if (hasProperty(union, 'items')) {
        const option = union; // Option1
    } else {
        const option = union; // Option2
    }

}

Alternatively, you can introduce a common property:

type Option1 = {
    tag: '1',
    items: string[];
}
type Option2 = {
    tag: '2',
    delete: true;
}
type Combined = Option1 | Option2;

type CommonProperty = Combined['tag'] // "1" | "2"

const handle = (union: Combined) => {
   if(union.tag==='1'){
       const option = union // Option1
   }
}

Another approach is to utilize StrictUnion.

type Option1 = {
    items: string[];
}
type Option2 = {
    delete: true;
}
type Combined = Option1 | Option2;

// credits goes https://stackoverflow.com/questions/65805600/type-union-not-checking-for-excess-properties#answer-65805753
type UnionKeys<T> = T extends T ? keyof T : never;

type StrictUnionHelper<T, TAll> =
    T extends any
    ? T & Partial<Record<Exclude<UnionKeys<TAll>, keyof T>, never>> : never;

type StrictUnion<T> = StrictUnionHelper<T, T>

type Union = StrictUnion<Combined>

const items_variable: Union['items'] = ["a", "b"]; // string[] | undefined

One drawback to note is that items_variable could also be undefined due to the nature of unions where they can hold one value or another interchangeably.

Answer №2

The issue arises from the fact that items is a valid property for only one of the union members. However, any operation involving Combined needs to be applicable to all union members. Hence, using Combined['items'] is not permissible.


To address this problem in your specific code scenario, it is recommended to avoid using the union altogether:

const items_variable: Option1["items"] = ["a", "b"];

Alternatively, you can define the items property for all union members, even if they have different types:

type Option1 = {
    items: string[];
}
type Option2 = {
    delete: true;
    items: never;
}
type Combined = Option1 | Option2;

const items_variable:Combined["items"] = ["a", "b"]; // works

Try Playground


Another approach could be utilizing an intersection to filter the union and focus on the type containing the desired property:

const items_variable: (Combined & { items: unknown })["items"] = ["a", "b"];

Or

const items_variable: (Combined & Option1)["items"] = ["a", "b"];

Try Playground


In order to access ['items'] within a type, every member of that type must possess that as a accessible property. The common aspect among these approaches is ensuring that each union member includes this property.

Answer №3

If you're searching for a different approach, perhaps consider using an Intersection field in place of the Union field:

    type Merged = Choice1 & Choice2;

Answer №4

Interactive TypeScript Playground

interface Option1 {
  items: string[];
}
interface Option2 {
  deleteAction: boolean;
}
type CombinedOptions = Option1 | Option2;

type KeysOfUnion<T> = T extends T ? keyof T : never;

type PropertyValue<T, K extends KeysOfUnion<T>> = K extends KeysOfUnion<T> ? T[K] : never

const itemsList: PropertyValue<CombinedOptions, 'items'> = ["apple", "banana"];

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

Hiding loading spinner in Ionic 2 upon iframe completion

I need to embed an external webpage in my app using an iframe. I have successfully implemented a loading animation while the iframe is loading, but I am unsure how to hide the animation once the iframe content has loaded. Unfortunately, I am unable to edit ...

Code error TS2345 occurs when assigning the argument of type '{ headers: HttpHeaders; }' to a parameter of type 'RequestOptionsArgs'. This indicates a mismatch in the type of data being passed, causing an

Upon running ionic serve, these are the results that I am encountering. My setup consists of Ionic4 version with Angular 8. While executing the command, this error appears: src/app/home/home.page.ts:60:77 - error TS2345: Argument of type '{ headers ...

What is the best way to trigger a mat-menu to open with just one click, while also automatically closing any other open menus at

I've encountered an issue where if there are multiple menus in the header, opening a menu for the first time works fine. However, if a menu is already open and I try to open another one, it doesn't work as expected. It closes the previously opene ...

Troubleshooting: The issue of importing Angular 2 service in @NgModule

In my Angular 2 application, I have created an ExchangeService class that is decorated with @Injectable. This service is included in the main module of my application: @NgModule({ imports: [ BrowserModule, HttpModule, FormsModu ...

Enable automatic suggestion in Monaco Editor by utilizing .d.ts files created from jsdoc

I am in the process of creating a website that allows users to write JavaScript code using the Monaco editor. I have developed custom JavaScript libraries for them and want to enable auto-completion for these libraries. The custom libraries are written in ...

LeafletModule was unable to be identified as an NgModule class within the Ivy framework

Currently working on an angular project using ngx-leaflet. Upon initiating ng serve in the terminal, the following error message pops up: Error: node_modules/@asymmetrik/ngx-leaflet/dist/leaflet/leaflet.module.d.ts:1:22 - error NG6002: Appears in the NgMod ...

Developing a separate NPM package for a portion of the app causes an increase in the total app size

Our projects utilize create-react-app and Typescript, resulting in the emergence of a collection of commonly used React components. To enhance maintenance and reusability, we decided to extract these components into their own NPM package named PackageA. Su ...

Is it possible to store any array of strings in localStorage?

I'm trying to save a list of addresses in local storage, like this: addresses["1 doe st, england","2 doe st, england", "3 doe st, england"] Each time I click on an address, I want it to be added to the array in local storage. However, my current imp ...

Trouble arises in TypeScript when defining a class - SyntaxError crops up

When I try to declare a class, I encounter an error: // The code below is from my test1.ts file class WDesign { wModel: string; wQuer: string; } let logWDesign = (wd : WDesign) => { console.log(wd.wModel + " " + wd.wQuer); } let wd1 : WDe ...

Child component in React not updating as expected

I am currently working with the following simplified components: export class StringExpression extends React.Component { render() { const fieldOptions = getFieldOptions(this.props.expression); console.log(fieldOptions); //I can see fieldOptions ...

Personalized Carousel using Ng-Bootstrap, showcasing image and description data fields

I have been working on customizing an Angular Bootstrap Carousel and have managed to successfully change the layout. I now have two columns - with the image on the right and text along with custom arrows on the left. My goal is twofold: First, I am lookin ...

Determining the dimensions of taskbar icons

How can I determine the size of taskbar buttons (small or large) on Windows 7 or 10? I have come across a registry key that might be helpful: HKEY_CURRENT_USER\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Advanced\ ...

Efficiently convert Map keys into a Set in Javascript without the need to copy and rebuild the set

Even though I am capable of const set = new Set(map.keys()) I don't want to have to rebuild the set. Additionally, I prefer not to create a duplicate set for the return value. The function responsible for returning this set should also have the abili ...

Ways to generate an Angular 7 component

Seeking guidance on creating an angular 7 component. I have forked a jsFiddle at this link: https://jsfiddle.net/gauravshrestha/fdxsywLv/. The chart in the fiddle allows data points to be dragged up and down. My goal is to convert this into a component whe ...

Enhance Your AG-Grid Experience with a Personalized Colorizing Pipe

I'm having an issue with my AG-Grid implementation in my app. I'm trying to use a custom color pipe that I created in one of the columns using cellRenderer. However, I'm encountering an error and I'm not sure how to fix it. Below is my ...

Collaborative service involves objects passing through reference

I am encountering an issue with a shared service in angular. Upon application startup, the init function is triggered, fetching and returning data that is vital across the entire application. Components have the ability to inject this service and retrieve ...

Error: Unable to retrieve URL from environment variable UPSTASH_REDIS_REST_URL in the parsing process

Encountering an issue with TypeScript while attempting to create a new Redis instance. I've set up an .env.local file with matching names for the redis URL and token. import { Redis } from '@upstash/redis' export const db: Redis = new Redis ...

Error: Certain Prisma model mappings are not being generated

In my schema.prisma file, I have noticed that some models are not generating their @@map for use in the client. model ContentFilter { id Int @id @default(autoincrement()) blurriness Float? @default(0.3) adult ...

What is the best way to connect a toArray function to an interface property?

Consider the following scenario: interface D { bar: string } interface B { C: Record<string, D> // ... additional properties here } const example: B = { C: { greeting: { bar: "hi" } } // ... additional properties here } Now I would like t ...

Exploring the wonders of using the Async Pipe with Reactive Extensions

I'm facing a little issue with the async pipe in Angular. Here's my scenario: I need to execute nested observables using the async pipe in HTML because I'm utilizing the on-push change detection strategy and would like to avoid workarounds ...