Associated interfaces with typing, yet distinct mandatory attributes

I frequently come across this particular pattern while working with the API. It involves having an object that, based on a specified type, will have certain properties that are always required, some that are optional, and some that are only required for a specific type. Typically, I address this issue by using the following approach (which may be clearer with some code examples):

export type FoobarTypes = 'foo' | 'bar';

export interface FooBarBase {
  id:        string;
  type:      FoobarTypes;
  optional?: any;
}

export interface FooBarFoo extends FooBarBase {
  foo: any;
}

export interface FooBarBar extends FooBarBase {
  bar: any;
}


export type FooBar = FooBarFoo | FooBarBar;


// differentiating between types:

export const isFooBarFoo  = (foobar: FooBar): foobar is FooBarFoo  =>
  (foobar as FooBarFoo).type === 'foo';

export const FooBarBar  = (foobar: FooBar): foobar is FooBarBar  =>
  (foobar as FooBarBar).type === 'bar';

While this method works well, I can't help but feel that it's somewhat complex and there might be a more efficient way to achieve the same result. Is there a better alternative?


Edit: This is just a refined version of the solution provided by @Fyodor. I'm including it here for easy reference, rather than buried in the comments, in case someone else has a similar question. His answer remains accurate, and I wouldn't have arrived at this version without his input.

export type FoobarTypes = 'foo' | 'bar';

// common properties shared among all types
export interface FooBarBase {
  id:        string;
  optional?: any;
}

// additional type-specific properties based on the type
export type FooBar<T extends FoobarTypes> =
  T extends 'foo' ? FooBarBase & {
    type: T;
    foo:  any;
  } : T extends 'bar' ? FooBarBase & {
    type: T;
    bar:  any;
  } : never;


// example usage...
function FB(fb: FooBar<FoobarTypes>) {
  if (fb.type === 'foo') fb.foo = '1';
  if (fb.type === 'bar') fb.bar = '2';
}

Answer №1

Utilizing discriminated unions and generics, you can implement completely different types based on the `type` property value.

export type FoobarTypes = 'foo' | 'bar';

type FooBarBase<T extends FoobarTypes> =
    T extends 'foo' ? 
    {
        id: string;
        type: T;   // T is 'foo'
        optional?: any;
        foo: any;
    } :
        T extends 'bar' ?
    {
        id: string;
        type: T;   // T is 'bar'
        optional?: any;
        bar: any;
    } : never;

type Foo = FooBarBase<'foo'>
type Bar = FooBarBase<'bar'>

function FB(fb: Foo | Bar) {
    if (fb.type === 'foo') {
        fb.foo = '1'   // TypeScript recognizes that fb has foo prop and doesn't have bar
    }
    else
    {
        fb.bar = '2'   // In this case, fb only contains 'bar'
    }
}

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

Is it advisable to send a response in Express.js or not?

When working with Express.js 4.x, I'm unsure whether to return the response (or next function) or not. So, which is preferred: Option A: app.get('/url', (req, res) => { res.send(200, { message: 'ok' }); }); Or Option B: ...

Error thrown due to missing property in type '{}' when using TypeScript arrow function parameter

As outlined in the documentation for interfaces in TypeScript, An interface declaration serves as an alternative way to define an object type. I'm puzzled by the error I encounter in the following code snippet. My attempt is to restrict the object ...

Having trouble consuming data from a service in Angular 6?

I'm in the process of creating a basic cache service in Angular; a service that includes a simple setter/getter function for different components to access data from. Unfortunately, when attempting to subscribe to this service to retrieve the data, t ...

How should one correctly set up a property using the @Input decorator in Angular 2?

I have developed a custom component in Angular to manage my material autocomplete functionality for selecting an Action. I am passing the Action[] from the parent component to this component. The autocomplete feature is working correctly, but I am encoun ...

After I deploy my Next.js code to Vercel, including Google Analytics added by @next/third-parties, I am encountering an error that does not appear in development mode

Lately, I completed a next.js project and integrated Google Analytics using @next/third-parties/google. During development, everything worked perfectly, but upon deploying it to vercel.com, an error popped up. ` ./app/layout.tsx:3 ...

Modify the code in JavaScript without utilizing the keyword "let"

Looking for alternative approaches to avoid using let in this code snippet due to the risks associated with variable reassignment leading to bugs that are hard to debug. Any suggestions? function findNodeById<F extends ISomeOptions>( optionsList ...

Instructing one class to delegate its redirect functions to another class

Within my JavaScript code, I have a class called class1 that takes in another class called class2 as a parameter in the constructor. My goal is to be able to access all the functions of class2 directly from class1, without having to manually declare each ...

What steps should I take to retrieve my information from a typescript callback function?

While I am familiar with using callbacks in JavaScript, I have recently started learning Angular and TypeScript. I am facing an issue with getting my data from a service back to where I need it after executing the callback function. The callback itself i ...

retrieving information from an array nested within a JSON object in an Angular application

I am struggling to retrieve two specific values from a JSON object. The content of the JSON is as follows: [ { "type":"session_start", "properties":[ { "property":"activity&q ...

When I attempt to return an object from a function and pass the reference to a prop, TypeScript throws an error. However, the error does not occur if the object is directly placed in

Currently, I have the following code block: const getDataForChart = () => { const labels = ['January', 'February', 'March', 'April', 'May', 'June', 'July']; const test = { ...

Employ the ctrl-v function to insert images directly into an input file

I am encountering an issue with using an input and a function to paste images. When I copy the URL of the image and paste it into the input using ctrl-v, I see the URL successfully. However, if I try to copy the actual image and paste it, ctrl-v does not ...

Steps for assigning the TAB key functionality within a text area

Is there a way to capture the TAB key press within a text area, allowing for indentation of text when the user uses it? ...

Eliminate any unnecessary padding from elements in angular2 components

Is there a way to remove the automatic padding added to new components in angular2? I am facing this issue with the header of my project, as shown in the image below: https://i.sstatic.net/25Zpn.png I attempted to eliminate the padding by setting it to 0 ...

Interface-derived properties

One of the challenges I'm facing is dealing with a time interval encapsulation interface in TypeScript: export interface TimeBased { start_time: Date; end_time: Date; duration_in_hours: number; } To implement this interface, I've created ...

Using createContext in React.tsx to pass the state through useState

There is a context called Transaction that accepts an object and a function as parameters. In the AppProvider component, the Transaction.Provider is returned. The following code snippet is from the GlobalState.tsx file: import { createContext, useState } f ...

What issue are we encountering with those `if` statements?

I am facing an issue with my Angular component code. Here is the code snippet: i=18; onScrollDown(evt:any) { setTimeout(()=>{ console.log(this.i) this.api.getApi().subscribe(({tool,beuty}) => { if (evt.index == ...

Error message: The ofType method from Angular Redux was not found

Recently, I came across an old tutorial on Redux-Firebase-Angular Authentication. In the tutorial, there is a confusing function that caught my attention: The code snippet in question involves importing Actions from @ngrx/effects and other dependencies to ...

Include "+5" in cases where additional elements cannot be accommodated

I am working on a project where I have a div called ItemsContainer that dynamically renders an array of items (Item) depending on the screen size. While mapping through the array, I want to check if there is enough space to display the current item. If no ...

`Running ng serve will result in the creation of a 'dist' folder within each app sub

Since beginning my project, I have encountered an issue that is both normal and frustrating. The dist folder is being created with incomplete information related to the components inside it. dashboard dist (unwanted) components panel dist (unwanted) c ...

Utilizing RxJS finalize in Angular to control the frequency of user clicks

Can someone provide guidance on using rxjs finalized in angular to prevent users from clicking the save button multiple times and sending multiple requests? When a button click triggers a call in our form, some users still tend to double-click, leading to ...