Typescript generic extends enum

Saved searches

Use saved searches to filter your results more quickly

You signed in with another tab or window. Reload to refresh your session. You signed out in another tab or window. Reload to refresh your session. You switched accounts on another tab or window. Reload to refresh your session.

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Suggestion: a better way to handle enums in generic functions #18869

Suggestion: a better way to handle enums in generic functions #18869

Comments

Currently, there’s no «easy» way to write a generic function that takes an enum as input/generic parameter and returns one of its constants. Consider the following example, which shows a way to almost accomplish it:

enum Color  RED = "#FF0000", GREEN = "#00FF00", BLUE = "#0000FF" > declare function randomKeyT>(object: T): keyof T; // One overload for numeric enums and one for string enums function randomEnumValueE extends [P in keyof E]: number>>(enumObject: E): number; function randomEnumValueE extends [P in keyof E]: string>>(enumObject: E): string; function randomEnumValue(enumObject: any): any  return enumObject[randomKey(enumObject)]; > declare function doSomethingWithColor(color: Color); let color = Color> randomEnumValue(Color); doSomethingWithColor(color);

The problem with this approach is that every call to randomEnumValue requires a cast to the enum type (the code would compile if Color was a numeric enum but color would still be a number , not a Color ). Also note that two overloads were required and the generic restriction E extends <[P in keyof E]: number>allows a lot of undesired types (i.e non-enums).

I suggest the following version:

function randomEnumValueT, E extends enumT>>(enumObject: typeof E): E  return enumObject[randomKey(enumObject)]; >

Here, T is the underlying type of the enum (currently, that means either number or string ). enum is a special type used to restrict a generic parameter to an enum of underlying type T . Also note that the parameter enumObject has type typeof E , which I think is currently not allowed and therefore is also part of my suggestion. That example would make it unnecessary to put a cast when calling that function, e.g

// calls randomEnumValue(enumObject: typeof Color): Color let color = randomEnumValue(Color); // color has type Color

Another possibility that crossed my mind is something like this:

function randomEnumValueT, E extends enumT>>(enumObject: E): EnumMemberE>  return enumObject[randomKey(enumObject)]; >

This might be easier to implement and I’m not sure which way would be preferable, but I feel like the overall idea of this suggestion would make enums better to manipulate.

The text was updated successfully, but these errors were encountered:

Источник

How to extend enums in TypeScript

Extending Enums in Typescript

TypeScript is well-loved by the developer community for many reasons, one of which is because of the static checks it provides to the code written in it.

Spotting problems early in your development lifecycle can save days of debugging random, vague errors that can sometimes pop up when using dynamic languages like JavaScript.

TypeScript can help make your code more predictable and better documented, make refactoring easier, and help reduce potential errors you might face at runtime in your production app. Its popularity and power are demonstrated by its 93% developer satisfaction rate and its skyrocketing usage over the past five years.

One language mechanism that is pivotal to TypeScript is enums. In this article, we’ll discuss:

What are enums in TypeScript?

Enums aren’t a feature of typing, interestingly, like most of TypeScript is — in fact, they are one of the few, new features that enhance the language.

Enums allow developers to define a strict set of options for a variable. For example:

Enums default to number enums, so the above enum is essentially an object with 0 , 1 , and 2 as its key, which we can see in the transpiled JavaScript code.

"use strict"; var Door; (function (Door) < Door[Door["Open"] = 0] = "Open"; Door[Door["Closed"] = 1] = "Closed"; Door[Door["Ajar"] = 2] = "Ajar"; // half open, half closed >)(Door || (Door = <>)); console.log(Door.FullyOpened);

In TypeScript, you can also use string enums, like so:

If you then used this Door enum, you could ensure that variables only used the three options specified in the enum. So, you couldn’t assign something incorrectly by accident or easily create bugs this way.

If you do try to use another variable, it will throw a type error like this:

enum Door < Open = "open", Closed = "closed", Ajar = "ajar" // half open, half closed >console.log(Door.FulyOpened)

Property ‘FullyOpened’ does not exist on type ‘typeof Door’.

Why do we need to extend an enum?

Extension is one of the four pillars of object orientation and is a language feature present in TypeScript. Extending an enum allows you to essentially copy a variable definition and add something extra to it.

So, for example, you might be trying to do something like this:

enum Door < Open = "open", Closed = "closed", Ajar = "ajar" // half open, half closed >enum DoorFrame extends Door < // This will not work! Missing = "noDoor" >console.log(DoorFrame.Missing)

We could then add extra properties into an enum, or even merge two enums together, to still get strict typing on our enum while also being able to change them after they’ve been defined.

But notice that the above code snippet doesn’t work! It fails to transpile and throws four different errors.

Can you extend enums?

The short answer is no, you can’t extend enums because TypeScript offers no language feature to extend them. However, there are workarounds you can utilize to achieve what inheritance would.

Type union in TypeScript

enum Door < Open = "open", Closed = "closed", Ajar = "ajar" // half open, half closed >enum DoorFrame < Missing = "noDoor" >type DoorState = Door | DoorFrame; let door: DoorState; door = Door.Ajar console.log(door) // 'ajar' door = DoorFrame.Missing console.log(door) // 'noDoor'

In the above code block, we used a union type. The union acts like an “or,” which simply means that the DoorState type will either be of type Door or of type DoorFrame .

This now means DoorState can use either of the variables from the two enums interchangeably.

However, an important caveat is that it can only use the types of the two enums. In TypeScript, it isn’t possible to use the values, like ajar and noDoor . You need to use the types like DoorFrame.Missing , which is a limitation.

Spread syntax

We have seen in the transpiled code earlier that an enum becomes a JavaScript object, with the keys and values that your enum specifies.

In TypeScript, we could write purely JavaScript if we wanted to. In fact, this is one big strength of TypeScript. You could, for example, rename all your file.js to file.ts and turn off the compiler checks for your code. As long as you run the compile/transpile steps, everything would work fine, with zero code changes.

Over 200k developers use LogRocket to create better digital experiences

Learn more →

We can make use of this by knowing that when our enum turns into JavaScript, it will be a JavaScript object literal and use the spread syntax, like below:

This solution has been described secondly, though, as it isn’t as good of a solution as the union type because it isn’t as robust as our first solution. This is because the “composition” of your enum is occurring at run time, whereas when we use type union, type checking can occur at compile/transpile time, not runtime.

TypeScript enum best practices

We have discussed how we can extend enums in Typescript, but enums aren’t a magic bullet to be used to fix all problems. Enums, when used incorrectly, can make your code readability, scalability, and maintainability worse, rather than improve your code.

So, let’s cover some best practices and common patterns to use when working with enums in TypeScript.

1. Avoid heterogenous enums

I have explained how we can have string enums like this:

Alongside also having numerical enums like this:

But, there is a third type of enum, which you may not be aware of, called a heterogenous enum. This is where you can use a string and numerical enums in the same enum.

An example from the docs is this:

enum BooleanLikeHeterogeneousEnum

It’s important to note that even the docs discourage this practice, as in this instance, using this method indicates you likely need to:

  • Reconsider the relationship between these two variables
  • Create two separate enums
  • Make them both conform to one data type

2. The “enums as configuration” anti-pattern

Sometimes code functionality can be forced to adhere to an enum option, which can quickly turn into an antipattern.

enum Operators < Add, Subtract >function calculate(op: Operators, firstNumber: number, secondNumber: number) < switch(op) < case Operators.Add: return firstNumber + secondNumber case Operators.Subtract: return firstNumber - secondNumber >>

The above code looks fairly simple and safe, because our example is, indeed, simple and safe.

But in large codebases, when you strictly tie implementation details to enum types like this, you can cause a few issues:

  • You create two sources of truth (both the enum and the function need to be updated if the enum changes)
  • This pattern is going to spread metadata around the code
  • The code block is no longer generic

If you need to do something like the above, a simpler (and more condensed) pattern could look like this.

const Operators = < Add: < id: 0, apply(firstNumber: number, secondNumber: number) < return firstNumber + secondNumber >>, Subtract: < id: 1, apply(firstNumber: number, secondNumber: number) < return firstNumber - secondNumber >> >

You can read more about this pattern here or here.

3. The types of data that enums best represent

There is a way of generally grouping together different types of data utilized in code: discrete variables or continuous variables.

Discrete variables are data that have spaces between their representations, and have only a few representations. Here are a few examples:

Discrete data is a good candidate to be placed inside an enum, and it can help code clarity and reuse. Continuous data refers to data without gaps that fall into a continuous sequence, like numbers. These can be different depending on their measurement:

Discrete data is a good candidate for data, and it can be used in an enum, whereas continuous data should not be used in an enum. Can you imagine an enum for age?

More great articles from LogRocket:

  • Don’t miss a moment with The Replay, a curated newsletter from LogRocket
  • Learn how LogRocket’s Galileo cuts through the noise to proactively resolve issues in your app
  • Use React’s useEffect to optimize your application’s performance
  • Switch between multiple versions of Node
  • Discover how to use the React children prop with TypeScript
  • Explore creating a custom mouse cursor with CSS
  • Advisory boards aren’t just for executives. Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.

This is not a good candidate to be placed in an enum because it will need to be continuously updated and amended, leading to a maintenance nightmare.

You should only look to add discrete, highly stable types of data inside an enum.

Conclusion

I hope this article has been useful so you better understand what enums are, the problems they solve, the use cases for merging two enums, and how you might achieve it! Happy hacking.

LogRocket: Full visibility into your web and mobile apps

LogRocket Dashboard Free Trial Banner

LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page and mobile apps.

Источник

Читайте также:  We didn find any interpreters python
Оцените статью