Utilize Typescript to ensure uniformity in object structure across two choices

Looking to create a tab component that can display tabs either with icons or plain text. Instead of passing in the variant, I am considering using Typescript to verify if any of the icons have an attribute called iconName. If one icon has it, then all other items must also include it (or else a compiler error will be thrown). It is considered valid if either all objects have iconName or none of them do.

Here's an example to illustrate my approach:

type ObjectWithIcon<T> = T extends { iconName: string } ? T & { iconName: string } : T;

interface MyObject {
  iconName?: string;
  label: string;
}

// Compiler error on the second object for missing iconName
const arrayOfObjects: ObjectWithIcon<MyObject>[] = [
  { iconName: "box", label: "storage" },
  { label: "memory" }, // Error: Property 'iconName' is missing, currently NOT throwing an error
];


// No errors as both objects have iconName
const arrayOfObjects2: ObjectWithIcon<MyObject>[] = [
  { iconName: "box", label: "storage" },
  { iconName: "circle", label: "memory" },
];

// No errors since no object uses iconName
const arrayOfObjects3: ObjectWithIcon<MyObject>[] = [
  { label: "storage" },
  { label: "memory" },
];

Answer №1

No matter how you define ObjectWithIcon<T>, it's best to avoid using the type ObjectWithIcon<MyObject>[]. This is simply a plain array type that doesn't consider whether its elements have a specific property or not. It only checks if each element adheres to the structure of ObjectWithIcon<MyObject> without looking at the overall array.

Instead, consider utilizing a union of array types. This means specifying that the array can be either one containing MyObjects with an iconName, or one containing MyObjects without an iconName.

To achieve this, you can create a type like:

type ObjectsAllWithOrAllWithoutIcon  = {
    iconName: string; // Required property
    label: string;
}[] | {
    iconName?: never; // Prohibited property 
    label: string;
}[]

While TypeScript doesn't directly support "without a given property", it does allow for declaring properties as optional with the impossible `never` type, which provides a similar outcome.


For this purpose, we can define utility types representing "with", "without", and the "either-or" scenario within the arrays:

type With<T, K extends keyof T> = T & Required<Pick<T, K>>;
type Without<T, K extends keyof T> = T & Partial<Record<K, never>>;
type ArrayAllWithOrAllWithout<T, K extends keyof T> = 
  With<T, K>[] | Without<T, K>[]

The With<T, K> type indicates that the object includes all properties from type T, plus the required property specified by K. On the other hand, Without<T, K> signals that the object possesses all properties from type T, with the option to exclude the property indicated by K through the use of the `never` type.

Finally, the

ArrayAllWithOrAllWithout<T, K>
denotes an array containing objects that are either instances of With<T, K>, or objects following the structure of Without<T, K>.


This structure ensures that:

interface MyObject {
  iconName?: string;
  label: string;
}    

type ObjectsAllWithOrAllWithoutIcon = 
  ArrayAllWithOrAllWithout<MyObject, "iconName">;

is equivalent to the manually defined ObjectsAllWithOrAllWithoutIcon mentioned earlier. You can then verify that your examples perform as intended:

const arrayOfObjects: ObjectsAllWithOrAllWithoutIcon = [ // error!
//    ~~~~~~~~~~~~~~
//  Property 'iconName' is missing in type '{ label: string; }' 
//  but required in type 'Required<Pick<MyObject, "iconName">>'.
  { iconName: "box", label: "storage" },
  { label: "memory" }, 
];

const arrayOfObjects2: ObjectsAllWithOrAllWithoutIcon = [
  { iconName: "box", label: "storage" },
  { iconName: "circle", label: "memory" },
]; // okay

const arrayOfObjects3: ObjectsAllWithOrAllWithoutIcon = [
  { label: "storage" },
  { label: "memory" },
]; // okay

Playground link to code

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

Merge two RxJS Observables/Subscriptions and loop through their data

Working with Angular (Version 7) and RxJS, I am making two API calls which return observables that I subscribe to. Now, the challenge is combining these subscriptions as the data from both observables are interdependent. This is necessary to implement cer ...

Why does it seem like Typescript Promise with JQuery is executing twice?

Figuring out Promises in Typescript/JS seemed to be going well for me, but I've hit a roadblock. I've set up Promises to wait for two JQuery getJSON requests to finish. In my browser, when connecting to a local server, everything functions as ex ...

When a URL is triggered via a browser notification in Angular 2, the target component ceases to function properly

Whenever I access a URL by clicking on a browser notification, the functionality of the page seems to stop working. To demonstrate this issue, I have a small project available here: https://github.com/bdwbdv/quickstart Expected behavior: after starting t ...

I am interested in transforming an Angular 2 observable into a custom class

Having recently delved into the world of angular2, I've spent countless hours trying to tackle a particular challenge without success. My goal is to convert an observable from an HTTP call and store it in a class. Below are the key components involve ...

Leveraging interfaces with the logical OR operator

Imagine a scenario where we have a slider component with an Input that can accept either Products or Teasers. public productsWithTeasers: (Product | Teaser)[]; When attempting to iterate through this array, an error is thrown in VS Code. <div *ngFor= ...

Having trouble resolving parameters? Facing an Angular dependency injection problem while exporting shared services?

Seeking to streamline the process of importing services into Angular 4 components, I devised a solution like this: import * as UtilityService from '../../services/utility.service'; As opposed to individually importing each service like so: imp ...

Can you identify the specific syntax for a 'set' function in TypeScript?

I have a TypeScript function that looks like this: set parameter(value: string) { this._paremeter = value; } It works perfectly fine. For the sake of completeness, I tried to add a type that specifies this function does not return anything. I experimen ...

Encountered an error with API request while utilizing Cashfree in a React Native environment

I'm currently integrating cashfree into my react native app for processing payments. Here is a snippet of the code I'm using: import { CFPaymentGatewayService, CFErrorResponse, } from 'react-native-cashfree-pg-sdk'; import { CFDr ...

Utilizing React with Typescript: A guide to working with Context

I have a super easy app snippet like import React, { createContext } from 'react'; import { render } from 'react-dom'; import './style.css'; interface IAppContext { name: string; age: number; country: string; } const A ...

Mastering the nesting of keys in Typescript.Unlock the secrets of

I encountered a situation where the following code snippet was causing an issue: class Transform<T> { constructor(private value: T) {} } class Test<T extends object> { constructor(private a: T) {} transform(): { [K in keyof T]: Transfo ...

Exception occurs when arrow function is replaced with an anonymous function

Currently, I am experimenting with the Angular Heroes Tutorial using Typescript. The code snippet below is functioning correctly while testing out the services: getHeroes() { this.heroService.getHeroes().then(heroes => this.heroes = heroes); } H ...

Is Webpack CLI causing issues when trying to run it on a .ts file without giving any error

I am facing an issue with my webpack.config.js file that has a default entrypoint specified. Here is the snippet of the configuration: module.exports = { entry: { main: path.resolve('./src/main.ts'), }, module: { rules: [ { ...

The argument provided needs to be a function, but instead, an object instance was received, not the original argument as expected

I originally had the following code: const util = require('util'); const exec = util.promisify(require('child_process').exec); But then I tried to refactor it like this: import * as exec from 'child_process'; const execPromis ...

Creating a Vue 3 Typescript project may lead to encountering the error message "this is undefined"

Just diving into Vue using Vite and TypeScript for my project, but running into errors during the build process. Most of them are Object is possibly 'undefined', particularly in parts of my template like this: <input :value="this.$store.s ...

Confirm the identity of a user by checking their email against the records stored in a MySQL database

I am currently working on creating a user verification system using email that is stored in a mySql database and utilizing express JS. The user is required to input their email before filling out any other forms. If the email is not found in the email tabl ...

The 'Subscription' type does not contain the properties _isScalar, source, operator, lift, and several others that are found in the 'Observable<any>' type

Looking to retrieve data from two APIs in Angular 8. I have created a resolver like this: export class AccessLevelResolve implements Resolve<any>{ constructor(private accessLevel: AccessLevelService) { } resolve(route: ActivatedRouteSnapshot, sta ...

What is preventing me from adding members to an imported declaration?

Currently, I am attempting to include a constructor in an imported declaration. As per the information provided in the documentation, this should be feasible. (Refer to Chapter Adding using an interface) Below is the code snippet that I have used: import ...

Mistakes that occur while trying to expand a base class to implement CRUD functionality

Creating a base class in TypeScript for a node.js application to be extended by all entities/objects for CRUD operations is my current challenge. The base class implementation looks like this: export class Base { Model: any; constructor(modelName ...

Angular 2's abstract component functionality

What are the benefits of utilizing abstract components in Angular 2? For example, consider the following code snippet: export abstract class TabComponent implements OnInit, OnDestroy {...} ...

When working with create-react-app and TypeScript, you may encounter an error stating: "JSX expressions in 'file_name.tsx' must

After setting up a React project with TypeScript using the CLI command create-react-app client --typescript, I encountered a compilation error when running npm start: ./src/App.js Line 26:13: 'React' must be in scope when using JSX react/r ...