Skip to content
New issue

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

array enumerations as runtime array + union type #48193

Closed
5 tasks done
amatiasq opened this issue Mar 9, 2022 · 11 comments
Closed
5 tasks done

array enumerations as runtime array + union type #48193

amatiasq opened this issue Mar 9, 2022 · 11 comments
Labels
Declined The issue was declined as something which matches the TypeScript vision Suggestion An idea for TypeScript

Comments

@amatiasq
Copy link

amatiasq commented Mar 9, 2022

Suggestion

Array enums

const X = enum ['a', 'b', 'c'];
// or
const X = ['a', 'b', 'c'] as enum;

🔍 Search Terms

Related tickets

✅ Viability Checklist

  • This wouldn't be a breaking change in existing TypeScript/JavaScript code
  • This wouldn't change the runtime behavior of existing JavaScript code
  • This could be implemented without emitting different JS based on the types of the expressions
  • This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, new syntax sugar for JS, etc.)
  • This feature would agree with the rest of TypeScript's Design Goals.

⭐ Suggestion

The idea is that if enum keyword is used in a constant array declaration, it leaves the array as runtime value but also returns the type as a union type of the values.

📃 Motivation

Currently enums are causing lots of issues by their nature that violates a few of the TypeScript design goals:

  • Impose no runtime overhead on emitted programs
  • Use a consistent, fully erasable, structural type system.

They require devs to create key-value pairs for each entry in the enum and that transpiles to a considerable amount of runtime code.

const enums came to help there but they lack the ability to get the list of valid values as a runtime array for runtime validation.

This can solve both issues with a terse, clean and removable syntax addition, and facilitates the usage of union types as enumerations.

💻 Use Cases

const Status = enum ['active', 'inactive', 'cancelled'];
// or
const Status = ['active', 'inactive', 'cancelled'] as enum;

Would transpile to...

// just `enum` striped
const Status = ['active', 'inactive', 'cancelled'];

.. and be identical to...

const Status = ['active', 'inactive', 'cancelled'] as const;
type Status = typeof Status[number];

That means that we can use Status both as runtime value:

function validate(status) {
  if (!Status.includes(status)) {
    throw new Error(`${status} is not a valid status: ${Status.join()}`);
  }
}

validate('cancelled') // ok
validate('hello') // Runtime error

And as a type

function set(status: Status) {
   // do something
}

set('active') // ok
set('hello') // type check error

Even combined

const isStatus = (x: unknown) : x is Status => Status.includes(x);

if (isStatus(queryParams.status)) {
  set(queryParams.status) // type narrowed to Status
}

Abstraction in code

This abstraction reaches the same goal but requires users to declare both the runtime array and the type in separated sentences and declare them with the same name.

type ValidKeys = string | number;

type Enum<T extends ValidKeys[]> = T[number];

function Enum<T extends ValidKeys[]>(...keys: [...T]) {
  return keys;
}

const Status = Enum('active', 'inactive', 'cancelled');
type Status = Enum<typeof Status>;

Comparison with enum & const enum

Declaration

enum Enum { A = 'a' }
const enum ConstEnum { A = 'a' }
const ArrayEnum = ['a'] as enum;

Usage as type

function enumFn(x: Enum) {}
function constEnumFn(x: ConstEnum) {}
function arrayEnumFn(x: ArrayEnum) {}

function EnumJsx(props: {x: Enum}) { return null }
function ConstEnumJsx(props: {x: ConstEnum}) { return null }
function ArrayEnumJsx(props: {x: ArrayEnum}) { return null }

Value usage

// Enum
enumFn(Enum.A);
enumFn('a' as Enum);
<EnumJsx x={Enum.A} />
<EnumJsx x={'a' as Enum} />

// ConstEnum
constEnumFn(ConstEnum.A);
constEnumFn('a' as ConstEnum); 
<ConstEnumJsx x={ConstEnum.A} />;
<ConstEnumJsx x={'a' as ConstEnum} />;

// ArrayEnum
arrayEnumFn('a');
<ArrayEnumJsx x="a" />;

Enumerate possible values at runtime

console.log(Object.values(Enum));
// not possible with ConstEnum
console.log(ArrayEnum);

Generated output

// Enum
var Enum;
(function (Enum) {
    Enum["A"] = "a";
})(Enum || (Enum = {}));

// ConstEnum
// no output

// ArrayEnum
const ArrayEnum = ['a'];

Alternative

Alternatively we can extend closer to the as const feature:

Discarded since as const is expression syntax (see comments)

const Status = ['active', 'inactive', 'cancelled'] as enum;
@amatiasq amatiasq changed the title enum type as runtime array enum as runtime array + union type Mar 9, 2022
@fatcerberus
Copy link

fatcerberus commented Mar 10, 2022

Since the proposed syntax has runtime representation, it violates

  • This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, new syntax sugar for JS, etc.)

Which you checked. The existing enum syntax is grandfathered because it was implemented before this rule was in place, but new runtime features that don't have a corresponding ES proposal will almost certainly be rejected.

@RyanCavanaugh RyanCavanaugh added Declined The issue was declined as something which matches the TypeScript vision Suggestion An idea for TypeScript labels Mar 10, 2022
@RyanCavanaugh
Copy link
Member

The intent for non-enum scenarios is really that you write the "... and be identical to..." block. Given an enum proposal already in the works in TC39 we don't want to possibly overstep that syntactical boundary until things settle (and likely wouldn't use up complexity for sugar of this granularity anyway)

@amatiasq
Copy link
Author

amatiasq commented Mar 10, 2022

Since the proposed syntax has runtime representation, it violates

The syntax doesn't have runtime representation since the syntax to be added is the enum keyword between = and the array. The rest is plain javascript.

The intent for non-enum scenarios is really that you write the "... and be identical to..." block.

const Status = ['active', 'inactive', 'cancelled'] as const;
type Status = typeof Status[number];

The reason to not do this is "requires users to declare both the runtime array and the type in separated sentences and declare them with the same name", unavoidable any other way.

I didn't know about the TC39 enum proposal. Is it this one? because this is Stage 0 with no updates since 2018.

This would still will be interesting, though, and wouldn't collide with such definition.

@fatcerberus
Copy link

fatcerberus commented Mar 10, 2022

You're proposing that the expression enum [ 'foo', 'bar' ] (which is not, in and of itself, valid JS syntax) transpile to something that's visible at runtime. That's precisely what's meant by "non-ECMAScript syntax with JavaScript output".

@amatiasq
Copy link
Author

amatiasq commented Mar 10, 2022

I fail to see why const x = enum ['y'] would be considered to "transpile to something that's visible at runtime" while const x = ['y'] as const would not (labeled as "This isn't a runtime feature" in the issue).

Anyway, would this be better?

const X = [ 'foo', 'bar' ] as enum;

At the end the runtime variable would be unchanged, we want to create a type X = 'foo' | 'bar' next to it, not alter the JS output.

I was trying to not add new keywords but maybe enum is not the right word here, is this more expressive?

const X = [ 'foo', 'bar' ] as union;

@amatiasq amatiasq changed the title enum as runtime array + union type array enumerations as runtime array + union type Mar 10, 2022
@fatcerberus
Copy link

fatcerberus commented Mar 10, 2022

Regardless of the choice of keyword, this is expression-level syntax. What do you propose should happen here:

foo(['x', 'y'] as enum);

If you intend that the creation of a new named type is tied specifically to the const x = ... then this would be better suited to dedicated syntax that makes that obvious, like enum type X = 'foo' | 'bar', and then it becomes a lot clearer why this feature has runtime impact.

@amatiasq
Copy link
Author

amatiasq commented Mar 11, 2022

foo(['x', 'y'] as enum);

Yes, this would be invalid syntax. I was thinking of this as a statement rather than an expression.

then this would be better suited to dedicated syntax that makes that obvious, like enum type X = 'foo' | 'bar'

The reason to not do that was exactly what you say, this would be TypeScript syntax generating runtime code.

That's not what this suggestion is about, it's about decorating an already existing, javascript valid, runtime declaration in such a way that TS knows a union type with the same name has to be created with it, any syntax would do as long as the Javascript code is still intact:

// as comment
const MyArray = ['a']; // @ts-enum
// or
const MyArray = ['a']; // ts please make this `as const` and union type from it

// As decorator if we could decorate variables
@union
const MyArray = ['a']

// Adding a keyword in what otherwise would be invalid syntax
const MyArray = ['a'] enum;
const MyArray = enum ['a'];
const MyArray enum = ['a'];

// Omited because it can be confused with `const enum`
// enum const MyArray = ['a']; 

I don't understand why would we say that

const MyArray = enum ['a']

has a runtime impact because "is not, in and of itself, valid JS syntax" when the same can be said about each one of the following sentences:

const x: number = 1;

function fn(x: number): number { return x }

const x = 1 as const;

function fn(x?) {}

myGenericFunction<string>();

and any other TS syntax that extends JS syntax to add type information. As I see it it's not that the syntax has runtime effect but the syntax decorates runtime code with type information.

@fatcerberus
Copy link

The difference from all those other examples is that here you're proposing to put enum [ 'a' ] in the expression position of a JS runtime construct (i.e. the initializer of a const), but then not make it a legal expression in general (you say the foo() call would be invalid syntax). That's pretty much a no-go since one of the explicit design goals of TS is to

  1. Avoid adding expression-level syntax.

https://github.com/Microsoft/TypeScript/wiki/TypeScript-Design-Goals

@amatiasq
Copy link
Author

@fatcerberus are you saying that if it's not part of the expresion side of the const initialiser it would be better?

const MyArray enum = ['a'];
const union MyArray = ['a'];
  1. Avoid adding expression-level syntax.

I checked that before and as said, it was never expected this to be an expression.

@zhujinxuan
Copy link

zhujinxuan commented Oct 3, 2022

Hi, I think the repetitive requests on enum is due to the need for Record<keyof typeof enum, Enum>. But currently, we have Record<keyof typeof enum, Enum> & Record<Enum, keyof typeof enum>.

I am not sure whether `Object.defineProperty´ would violate non-goals, like this

oneWay enum Weekdays {
  Mon, 
}

would compile to

(function (Weekdays) {
// if Weekdays.Mon !== "Mon"
Object.defineProperty(Weekdays, 0, {value: "Mon", enumerable: false} )
})(Weekdays || Weekdays = {})

But it might not be very compatible with early ES. But we can enable this keyword when and only when the target supports Objcet.defineProperty

@typescript-bot
Copy link
Collaborator

This issue has been marked as "Declined" and has seen no recent activity. It has been automatically closed for house-keeping purposes.

@typescript-bot typescript-bot closed this as not planned Won't fix, can't repro, duplicate, stale Jun 21, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Declined The issue was declined as something which matches the TypeScript vision Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

6 participants
@zhujinxuan @amatiasq @fatcerberus @RyanCavanaugh @typescript-bot and others