An array of objects in Typescript utilizing a generic type with an enum

Here’s a glimpse of code that showcases the issue:

enum ServicePlugin {
  Plugin1,
  Plugin2,
  Plugin3,
}

interface PluginOptions {
  [ServicePlugin.Plugin1]: { option1: string };
  [ServicePlugin.Plugin2]: { option1: number; option2: number };
}

type PluginOptionsMap = Omit<
  { [key in ServicePlugin]: undefined },
  keyof PluginOptions
> &
  PluginOptions;

type PluginConfig<T extends ServicePlugin> =
  PluginOptionsMap[T] extends undefined
    ? { plugin: T }
    : {
        plugin: T;
        options: PluginOptionsMap[T];
      };

const params: PluginConfig<ServicePlugin>[] = [
  {
    plugin: ServicePlugin.Plugin1,
    options: {
      option1: "foo",
    },
  },
  {
    plugin: ServicePlugin.Plugin2,
    options: {
      option1: 123,
      option2: 456,
    },
  },
  {
    plugin: ServicePlugin.Plugin2,
    options: {
      option1: "bar", // No error here even though it is the wrong options type
    },
  },
  {
    plugin: ServicePlugin.Plugin3,
    // Error: 'options' is not declared here
  },
];

The

PluginConfig<ServicePlugin>[]
type acts more like an array of all plugin unions rather than individual plugins. Although logical, it’s not what I need.

How can I form an array of generic objects where each object has a specific enum value as its generic type?

Answer №1

If you are looking to define PluginConfig as a specific (non-generic) union type that aligns with the members of the ServicePlugin union enum, consider creating it this way:

type PluginConfig = {
    plugin: ServicePlugin.Plugin1;
    options: {
        option1: string;
    };
} | {
    plugin: ServicePlugin.Plugin2;
    options: {
        option1: number;
        option2: number;
    };
} | {
    plugin: ServicePlugin.Plugin3;
    options?: never;
}

In this definition, notice how in the case of Plugin3, the options property is made an optional property with the impossible never type. This structure effectively restricts the presence of the options property, allowing only undefined or its absence.


To generate the PluginConfig type directly from the definitions of ServicePlugin and PluginOptions, utilizing a distributive object type method, you can employ the following logic inspired by microsoft/TypeScript#47109:

type PluginConfig = { [K in ServicePlugin]:
    K extends keyof PluginOptions ?
    { plugin: K, options: PluginOptions[K] } :
    { plugin: K, options?: never }
}[ServicePlugin];

This approach uses a mapped type over ServicePlugin followed by an immediate index operation onto it with ServicePlugin, which essentially distributes the operation across the ServicePlugin union for a consolidated result.

The conditional operation applied here,

K extends keyof PluginOptions ? { plugin: K, options: PluginOptions[K] } : { plugin: K, options?: never }
, ensures that the options property varies based on whether K is a key within PluginOptions, maintaining type integrity across the union.

You can verify that this setup produces an equivalent type as previously described.


To confirm functionality, let's put it to the test with some example usage:

const params: PluginConfig[] = [
    {
        plugin: ServicePlugin.Plugin1,
        options: {
            option1: "foo",
        },
    },
    {
        plugin: ServicePlugin.Plugin2,
        options: {
            option1: 123,
            option2: 456,
        },
    },
    {
        plugin: ServicePlugin.Plugin2,
        options: {
            option1: "bar", // error,
        },
    }, // Types of property 'options' are incompatible.
    {
        plugin: ServicePlugin.Plugin3, // okay
    },
];

The validation results look promising. Errors will be flagged, such as the attempt to use options intended for ServicePlugin.Plugin1 with ServicePlugin.Plugin2, while omissions like the optional options property are permitted.

Check out Playground link

Answer №2

Alright, buckle up because this is going to be a long one...

Reorganization

To begin with, I reorganized some of the types in a more streamlined manner without altering their functionality significantly. This makes it easier to understand and manage intricate types.

The current approach to computing the PluginOptionsMap seems a bit convoluted. Essentially, it aims to fill all empty keys with undefined.

type PluginOptionsMap = Omit<   
  { [key in ServicePlugin]: undefined },    
  keyof PluginOptions   
> & 
  PluginOptions;

A cleaner alternative would involve iterating over the keys of the ServicePlugin enum. For each key, check if it exists within PluginOptions. If it does, return the corresponding options; otherwise, return undefined.

type PluginOptionsMap = {
  [key in ServicePlugin]: key extends keyof PluginOptions
    ? PluginOptions[key] : undefined
};

Side note - While using 'key' as a generic type works, utilizing single capital letters like 'T' for generics can improve readability, especially in complex TypeScript codebases. Refer to this link for more insights.

Ultimately, the choice is yours - just a helpful tip :)

Tackling Issues

During troubleshooting sessions where I encounter type errors, assigning the troublesome type to a variable and inspecting its properties via hover helps pinpoint the root cause effectively.

Throughout the process, I introduce intermediate types for clarity and validation purposes. Feel free to remove them once your types are refined.

Resolution

Upon analyzing the resulting type of PluginConfig\<ServicePlugin\>[], the structure unveiled...

const testParams: {
    plugin: ServicePlugin;
    options: {
        option1: string;
        option2?: undefined;
    } | {
        option1: number;
        option2: number;
    } | undefined;
}[]

An issue arises whereby TypeScript assigns the union at the Params.options level instead of the topmost level. Essentially, any plugin value from ServicePlugin and any compatible options value is acceptable.

To enforce the creation of unions at the highest level, we design a mapping strategy similar to that used for PluginOptionsMap but with tailored structuring.

type PluginConfigMap = {
  [T in keyof PluginOptionsMap]: PluginOptionsMap[T] extends undefined ? { plugin: T } : {
    plugin: T;
    options: PluginOptionsMap[T];
  }
};

This resulting type takes shape as follows...

type PluginConfigMap = {
    0: {
        plugin: ServicePlugin.Plugin1;
        options: {
            option1: string;
        };
    };
    1: {
        plugin: ServicePlugin.Plugin2;
        options: {
            option1: number;
            option2: number;
        };
    };
    2: {
        plugin: ServicePlugin.Plugin3;
    };
}

Subsequently, we extract the values from PluginConfigMap.

import { ValuesType } from 'utility-types';

type PluginConfigs = ValuesType<PluginConfigMap>

Here, I leverage the ValuesType helper type from utility-types. It's a recommended resource for understanding and simplifying type definitions.

With the new PluginConfigs type in play, the anticipated outcome materializes...

https://i.stack.imgur.com/X9IZh.png

For an in-depth exploration of the topic, refer to the complete solution playground https://www.typescriptlang.org/play?#code/JYWwDg9gTgLgBAbzgNQIYBsCuBTAzgFQE8xs4BfOAMyghDgHJMZh1gZCBadk3egbgCwAKGHYAdpjoBlbFABuwAMbYAClgDmwMYmFw4azJrEBGADS79GrQCZzQvQaMBmO2WHCtMWZVTLLhrQB5MGYIMVwdezgAbRl5JVUrMQA6Ry...undai\

Further Insights

  • At times, dealing with unions of objects may pose challenges in differentiation. Addressing this involves representing each property across all objects within the union. Explore related articles for detailed guidance.

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

Angular 2: Issue with Table not Being Updated

https://i.stack.imgur.com/qLDUZ.png The UsersList component opens a new page upon clicking the table. https://i.stack.imgur.com/WwqIX.png After changing and saving user values, the updated value is not immediately reflected in the grid for the first tim ...

Having trouble with GraphQL Explorer and Express Sessions compatibility?

Struggling to implement a login system using GraphQL and Express, but facing issues with session persistence. Despite logging in, req.session.userId remains undefined. Code snippet: (async () => { await connect(process.env.MONGO_URI!, { dbName: "ex ...

Different methods for organizing an array of strings based on eslint/prettier

I possess an assortment of keys that I desire to sort in alphabetical order whenever I execute eslint --fix/prettier. My inference is that such a feature does not exist by default due to its potential impact on the code's behavior. Therefore, my quer ...

Attempting to access a specific JSON key using Observables

Apologies for the question, but I'm relatively new to Typescript and Ionic, and I find myself a bit lost on how to proceed. I have a JSON file containing 150 entries that follow a quite simple interface declaration: export interface ReverseWords { id ...

Can one extract a property from an object and assign it to a property on the constructor?

Currently working with TypeScript, I am looking to destructure properties from an object. The challenge lies in the fact that I need to assign it to a property on the constructor of the class: var someData = [{title: 'some title', desc: 'so ...

Harness the power of the Node.js Path module in conjunction with Angular 6

I'm currently facing an issue with utilizing the Path module in my Angular 6 project. After some research, I came across a helpful post detailing a potential solution: https://gist.github.com/niespodd/1fa82da6f8c901d1c33d2fcbb762947d The remedy inv ...

Typescript: Issue encountered with Record type causing Type Error

When utilizing handler functions within an object, the Record type is used in the following code snippet: interface User { id: string; avatar: string; email: string; name: string; role?: string; [key: string]: any; } interface Stat ...

Creating a versatile TypeScript Record that can accommodate multiple data types

I have a question regarding the use of Record in TypeScript. When I specify Record as the parameter type in a function, I encounter an error in my code because it does not allow different types. type Keys = 'name' | 'qty'; const getVal ...

Prisma : what is the best way to retrieve all elements that correspond to a list of IDs?

I'm currently implementing Prisma with NextJs. Within my API, I am sending a list of numbers that represent the ID's of objects in the database. For example, if I receive the list [1, 2, 12], I want to retrieve the objects where the ID is eithe ...

Description: TypeScript type that derives from the third constructor parameter of a generic function

How can I determine the type of constructor props for a generic type? Take a look at this example. type PatchableProps<T> = T extends { [k: string | number]: any } ? { [Key in keyof T]: PatchableProps<T[Key]> } : T | Patch export class ...

Unable to break down the property 'desks' of '(0 , _react.useContext)(...)' due to its undefined nature

Trying to mock DeskContext to include desks and checkIfUserPresent when calling useContext is causing an error to occur: Cannot destructure property 'desks' of '(0 , _react.useContext)(...)' as it is undefined TypeError: Cannot destruct ...

Having trouble updating the value in the toolbar?

Here is an example that I added below: When I click the add button, the value of the "newarray" variable is updated but not reflected. I am unsure how to resolve this issue. This function is used to add new objects: export class AppComponent { ne ...

Mongoose: An unexpected error has occurred

Recently, I developed an express app with a nested app called users using Typescript. The structure of my app.js file is as follows: ///<reference path='d.ts/DefinitelyTyped/node/node.d.ts' /> ///<reference path='d.ts/DefinitelyTyp ...

Encountering a Problem with vue-check-view Library in Typescript

After successfully declaring the js plugin with *.d.ts, I encountered an issue where my view was blank after using .use(checkView). Does the library vue-check-view support Typescript? Error: Uncaught TypeError: Cannot read property '$isServer' o ...

Windows drive letter casing error when using Yarn and NextJS

Today, I set up a fresh project using Yarn and NextJS on my Windows computer. When I try to start the project, I encounter an error stating that the casing is "invalid" for the project directory. The specific errors I am facing are: Invalid casing detecte ...

Ways to create a fixed button positioned statically at the bottom of a page

Currently, I am utilizing tailwind CSS to create a webpage with Next and Back buttons for navigation. However, an issue arises when there is minimal content on the page as the button adheres to the top. For visual reference, please view the image linked be ...

Why does the Angular page not load on the first visit, but loads successfully on subsequent visits and opens without any issues?

I am currently in the process of converting a template to Angular that utilizes HTML, CSS, Bootstrap, JavaScript, and other similar technologies. Within the template, there is a loader function with a GIF animation embedded within it. Interestingly, upon ...

Exploring the functionalities of R.pick in TypeScript

Recently, I attempted the following code snippet: import R from 'ramda' import fs from 'fs' import path from 'path' import {promisify} from 'util' const readFile = promisify(fs.readFile) export async function disc ...

Having trouble with Angular 2 when submitting a form

Whenever I try to submit a form with two input fields and one select list, the submitted value for the select list is always 0. I am unsure where I am making a mistake. When I post this form to an API, all values are correct except for the select list valu ...

Angular 2 implementes a loading spinner for every HTTP request made

My objective is to implement a spinner functionality whenever an HTTP request occurs in my Angular app. Essentially, I want the user to see a loading screen during these requests within my app component. The setup for my spinner component and spinner servi ...