How can you ensure an interface in typescript 3.0 "implements" all keys of an enum?

Imagine I have an enum called E { A = "a", B = "b"}. I want to enforce that certain interfaces or types (for clarity, let's focus on interfaces) include all the keys of E. However, I also need to specify a separate type for each field. Therefore, using { [P in E]: any } or { [P in E]: T } is not the ideal solution.

For example, there could be two interfaces implementing E:

  • E { A = "a", B = "b"}
  • Interface ISomething { a: string, b: number}
  • Interface ISomethingElse { a: boolean, b: string}

As E expands during development, it might evolve to:

  • E { A = "a", B = "b", C="c"}
  • Interface ISomething { a: string, b: number, c: OtherType}
  • Interface ISomethingElse { a: boolean, b: string, c: DifferentType}

And later on:

  • E { A = "a", C="c", D="d"}
  • Interface ISomething { a: string, c: ChosenType, d: CarefullyChosenType}
  • Interface ISomethingElse { a: boolean, c: DifferentType, d: VeryDifferentType}

This pattern continues to update over time. Thus, based on information from https://www.typescriptlang.org/docs/handbook/advanced-types.html, it appears that this feature is not yet supported. Are there any TypeScript tricks that I may be overlooking?

Answer №1

It appears that you are determined to manually list out both the enum and the interface, and then rely on TypeScript to alert you if the interface is missing keys from the enum (or vice versa).

For instance, suppose you have:

enum Colors { Red = "red", Green = "green", Blue = "blue"};
interface ColorScheme { red: string, green: number, blue: OtherType};

You can utilize conditional types to enable TypeScript to determine if any elements of Colors are absent from the keys of ColorScheme:

type MissingKeysFromColorScheme = Exclude<Colors, keyof ColorScheme>;

If there are no missing keys in ColorScheme, this type should be never. Otherwise, it will correspond to one of the values of Colors, such as Colors.Blue.

You can also instruct the compiler to identify whether ColorScheme contains any keys that do not align with constituents of Colors, though this requires a more convoluted process due to limitations when manipulating enums programmatically. Here's how you can achieve this:

type ExtraKeysInColorScheme = { 
  [K in keyof ColorScheme]: Extract<Colors, K> extends never ? K : never 
}[keyof ColorScheme];

Similarly, this type should be never if there are no extra keys. To prompt a compile-time error if either of these is not never, employ generic constraints along with default type parameters like so:

type ValidateColorScheme<
  Missing extends never = MissingKeysFromColorScheme, 
  Extra extends never = ExtraKeysInColorScheme
> = 0;

Although the type ValidateColorScheme itself may seem trivial (it evaluates to

0</code), the generic parameters <code>Missing
and
Extra</code provide feedback on errors if their defaults are not <code>never
.

Test it out by implementing:

enum Colors { Red = "red", Green = "green", Blue = "blue" }
interface ColorScheme { red: string, green: number, blue: OtherType }
type VerifyColorScheme<
  Missing extends never = MissingKeysFromColorScheme,
  Extra extends never = ExtraKeysInColorScheme
  > = 0; // no errors

Compare this with:

enum Colors { Red = "red", Green = "green", Blue = "blue" }
interface ColorScheme { red: string, green: number } // Oops, 'blue' is missing
type VerifyColorScheme<
  Missing extends never = MissingKeysFromColorScheme, // Error!
  Extra extends never = ExtraKeysInColorScheme
  > = 0; // The constraint is not satisfied by 'Blue'

And another scenario:

enum Colors { Red = "red", Green = "green", Blue = "blue" }
interface ColorScheme { red: string, green: number, blue: OtherType, indigo: 1} // Oops, 'indigo' is an extra key
type VerifyColorScheme<
  Missing extends never = MissingKeysFromColorScheme,
  Extra extends never = ExtraKeysInColorScheme // Error!
  > = 0; // 'Indigo' does not satisfy the constraint

While these solutions are functional, they are admittedly not elegant.


An alternative approach involves using a dummy class solely for the purpose of prompting an error if the correct properties are not included:

enum Shapes { Circle = "circle", Square = "square" , Triangle = "triangle"};
class ShapePlan implements Record<Shapes, unknown> {
  circle!: string;
  square!: number;
  triangle!: boolean;
}
interface ShapeBlueprint extends ShapePlan {}

If any properties are omitted, an error will be triggered:

class ShapePlan implements Record<Shapes, unknown> { // Error!
  circle!: string;
  square!: number;
}
// Class 'ShapePlan' does not correctly implement interface 'Record<Shapes, unknown>'.
// Property 'triangle' is missing in type 'ShapePlan'.

However, this method does not flag extra properties, which may or may not be pertinent in your case.


Hopefully one of these approaches proves useful for your situation. Best of luck!

Answer №2

To utilize a mapped type with the enum, you can do the following:

enum E { A = "a", B = "b"}

type AllE = { [P in E]: any }

let o: AllE = {
    [E.A]: 1,
    [E.B]: 2
};

let o2: AllE = {
    a: 1,
    b :2
}

Playground link

Update

If you wish to maintain the original property types of the new object, you need to use a function. This function helps infer the actual type of the newly created object literal while ensuring it has all keys of E

enum E { A = "a", B = "b" };


function createAllE<T extends Record<E, unknown>>(o: T) : T {
    return o
}

let o = createAllE({
    [E.A]: 1,
    [E.B]: 2
}); // o is  { [E.A]: number; [E.B]: number; }

let o2 = createAllE({
    a: 1,
    b: 2
}) // o2 is { a: number; b: number; }


let o3 = createAllE({
    a: 2
}); // error

Playground link

Answer №3

Utilize a Record. Simply place the Enum type in the initial position without utilizing keyof or any other method.

export enum MyEnum {
  ONE = 'One',
  TWO = 'Two',
}

export const DISPLAY_MAPPING: Record<MyEnum, string> = {
  [MyEnum.ONE]: 'One Mapping',
  [MyEnum.TWO]: 'Two Mapping',
}

If you omit one of the properties, TypeScript will generate an error message.

Answer №4

If the use of a string enum is not desirable, an alternative approach would be as shown below:

enum E {
  X,
  Y,
  Z,
}

type SomeType = Record<keyof typeof E, number>;

const values: SomeType = {
  X: 10,
  Y: 20,
  Z: 30,
};

Answer №5

For those comfortable using a type (instead of an interface), you can easily achieve the desired outcome by leveraging the native Record type:

enum Letter { X = "x", Y = "y", Z="z"}
type Something = Record<Letter, { x: string, y: number, z: AnotherType}>
type SomethingElse = Record<Letter, { x: boolean, y: string, z: DifferentType}>

By implementing it in this manner, TypeScript will provide warnings if any keys from the enum are missing in either Something or SomethingElse.

Answer №6

The exceptional response provided by jcalz, the most highly rated answer above, fails to utilize the satisfies operator introduced in TypeScript version 4.9 specifically to tackle this very issue.

To implement it, you can follow this syntax:

enum MyEnum {
  Value1,
  Value2,
  Value3,
}

const myObject = {
  [MyEnum.Value1]: 123,
  [MyEnum.Value2]: 456,
  [MyEnum.Value3]: 789,
} satisfies Record<MyEnum, number>;

This eliminates the need for unnecessary boilerplate code present in other responses.

However, please note that the satisfies operator cannot be applied directly on an interface or type. In such cases, a helper function like the following can be used:

enum MyEnum {
  Value1,
  Value2,
  Value3,
}

interface MyInterface {
  [MyEnum.Value1]: 123,
  [MyEnum.Value2]: 456,
  [MyEnum.Value3]: 789,
};

validateInterfaceMatchesEnum<MyInterface, MyEnum>();

You can then include this helper function in your standard library, which has the following structure:

// Code for validating interface matches with enum
export function validateInterfaceMatchesEnum<
  T extends Record<Enum, unknown>,
  Enum extends string | number,
>(): void {}

(Alternatively, you can incorporate the npm dependency isaacscript-common-ts, a library that offers useful helper functions.)


ARCHIVED ANSWER:

jcalz's excellent solution mentioned earlier (the top-voted answer) is no longer compatible with recent versions of TypeScript, resulting in the error message:

All type parameters are unused. ts(62305)

This occurs due to the unused generic parameters. The problem can be resolved by prefixing the variable names with an underscore character, as shown below:

// Cloning necessary objects for verification reuse
type EnumToCheck = MyEnum;
type InterfaceToCheck = MyInterface;

// Trigger compiler error if InterfaceToCheck does not align with EnumToCheck values
type KeysMissing = Exclude<EnumToCheck, keyof InterfaceToCheck>;
type ExtraKeys = {
  [K in keyof InterfaceToCheck]: Extract<EnumToCheck, K> extends never
    ? K
    : never;
}[keyof InterfaceToCheck];
type Verify<
  _Missing extends never = KeysMissing,
  _Extra extends never = ExtraKeys,
> = 0;

Note: If using ESLint, some lines might require an eslint-disable-line comment due to the unused "Verify" type and generic parameters.

Answer №7

If you're satisfied with the Typescript compiler throwing an error when not all enum values are utilized as interface keys, this concise method can help achieve that:

const validateKeys = SomeInterface[SomeEnumValues];

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

A guide to building a versatile component using Ionic 3 and Angular 4

I decided to implement a reusable header for my app. Here's how I went about it: First, I created the component (app-header): app-header.ts: import { Component } from '@angular/core'; @Component({ selector: 'app-header', te ...

While working with Ngrx/effects, an error with code TS2345 occurred. The error message stated that the argument is of type 'Product[]', which cannot be assigned to a parameter of type

When I compile my code, I encounter the following issue (despite not finding any errors in the browser console and the application functioning properly). An error occurs in src/app/services/product.service.ts(15,9): The type 'Observable<Product> ...

Issues with Angular displaying filter incorrectly

Whenever a user chooses a tag, I want to show only the posts that have that specific tag. For instance, if a user selects the '#C#' tag, only posts with this tag should be displayed. This is how my system is set up: I have an array of blogs that ...

What is the process for destructuring an interface in TypeScript?

My request is as follows: const listCompartmentsRequest: identity.requests.ListCompartmentsRequest = { compartmentId: id, compartmentIdInSubtree: true, } I want to shorten the long line identity.requests.ListCompartmentsRequest. I'm looking for ...

utilize the getStaticProps function within the specified component

I recently started a project using Next.js and TypeScript. I have a main component that is called in the index.js page, where I use the getStaticProps function. However, when I log the prop object returned by getStaticProps in my main component, it shows a ...

The default value for the ReturnType<typeof setInterval> in both the browser and node environments

I have a question about setting the initial value for the intervalTimer in an Interval that I need to save. The type is ReturnType<typeof setInterval>. interface Data { intervalTimer: ReturnType<typeof setInterval> } var data : Data = { ...

Tips on effectively transferring formarray to another component

I'm attempting to pass a formarray to a child component in order to display the values within the formarray there. Here is my current code, but I am struggling to figure out how to show the formarray values in the child component. app.component.html ...

In Typescript, we can use a class to encapsulate another class and then return a generic result

Trying to wrap my head around the concept of generic classes, and now I need to dynamically create another class. However, I am uncertain about how to go about it. Class A {} Class B<T> { Return T } Const c = B(A); // which is T More context on w ...

Toggle the visibility of an input field based on a checkbox's onchange event

I am facing a challenge where I need to display or hide an Input/Text field based on the state of a Checkbox. When the Checkbox is checked, I want to show the TextField, and when it is unchecked, I want to hide it. Below is the code snippet for this compon ...

Exploring ways to fetch an HTTP response using a TypeScript POST request

I have been looking at various questions, but unfortunately, none of them have provided the help I need. The typescript method I am currently working with is as follows: transferAmount(transfer: Transfer): Observable<number> { return this.http .po ...

Determining the presence of generic K within generic M in Typescript Generics and Redux

Hello there I am currently working on minimizing repetitive code in my react application by utilizing Redux state. After choosing the Redux structure to use (refer to Context), I now aim to make it more concise. To achieve this, I have developed a generic ...

Leveraging the power of literal types to choose a different type of argument

My API is quite generic and I'm looking for a typed TypeScript client solution. Currently, my code looks like this: export type EntityTypes = | 'account' | 'organization' | 'group' export function getListByVa ...

Inserting a pause between a trio of separate phrases

I am dealing with three string variables that are stacked on top of each other without any spacing. Is there a way to add something similar to a tag in the ts file instead of the template? Alternatively, can I input multiple values into my angular compo ...

Checking React props in WebStorm using type definitions

Currently, I am utilizing WebStorm 2018.3.4 and attempting to discover how to conduct type checking on the props of a React component. Specifically, when a prop is designated as a string but is given a number, I would like WebStorm to display an error. To ...

Passing properties to a component from Material UI Tab

I have been attempting to combine react-router with Material-UI V1 Tabs, following guidance from this GitHub issue and this Stack Overflow post, but the solution provided is leading to errors for me. As far as I understand, this is how it should be implem ...

Display JSON data in a table format using Angular

I have received a JSON result that looks like this: { "discipline":"ACLS", "course": [ { "value":"ACLS Initial", "classes":"0", "students":"0" }, { "BLS":"CPR Inst ...

Encountering errors while setting up routes with Browser Router

When setting up a BrowserRouter in index.tsx, the following code is used: import './index.css'; import {Route, Router} from '@mui/icons-material'; import {createTheme, ThemeProvider} from '@mui/material'; import App from &ap ...

I'm encountering a 404 error on Next.js localhost:3000

Embarking on a fresh project in Next.js, my folder structure looks like this: https://i.stack.imgur.com/HhiJo.png However, upon navigating to localhost:3000, I am greeted with a 404 error screen. It seems there is an issue with the routing, but unfortuna ...

Components for managing Create, Read, Update, and Delete operations

As I embark on my Angular 2 journey with TypeScript, I am exploring the most efficient way to structure my application. Consider a scenario where I need to perform CRUD operations on a product. Should I create a separate component for each operation, such ...

Issue with applying value changes in Timeout on Angular Material components

I'm currently experimenting with Angular, and I seem to be struggling with displaying a fake progress bar using the "angular/material/progress-bar" component. (https://material.angular.io/components/progress-bar/) In my "app.component.html", I have m ...