Skip to content

Commit e9ac589

Browse files
committed
Added tuples to ButterKeyedEnums with tupleFactory
1 parent d897bab commit e9ac589

9 files changed

Lines changed: 334 additions & 32 deletions

File tree

README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,19 @@ const Fruits = ButterKeyedEnum({
7575
color: 'yellow',
7676
sweetness: 2
7777
}
78+
}, {
79+
// This is required because typescript cannot convert from a union to a tuple with
80+
// * Guaranteed order
81+
// * Better performance than O(n^2)
82+
// See https://stackoverflow.com/questions/55127004/how-to-transform-union-type-to-tuple-type
83+
//
84+
// So, we have to provide a tuple factory. It constrains the tuple to make sure you're not missing any values.
85+
// Making our typescript compiler happy.
86+
tupleFactory: (enumObject) => [
87+
enumObject.apple,
88+
enumObject.lemon,
89+
enumObject.banana,
90+
]
7891
});
7992

8093
// Access enum values

dist/index.d.ts

Lines changed: 57 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -74,22 +74,72 @@ export declare function ButterTupleEnum<const T extends readonly string[]>(tuple
7474
* colorsEnum.keys // ['green']
7575
* ```
7676
*
77-
* @template TEnum The object type containing enum entries
78-
* @returns The keyed enum object with helper methods
77+
* @template KeyName - The name of the property to hoist the key into. Defaults to `"key"`.
78+
* @template T - The original enum-like object whose keys should not conflict with `keyName` in inner objects.
79+
* @template TTuple - The tuple of values derived from `T` that will represent the enum values.
80+
* @template TResult - The final result tuple validated against the keys of `T`.
81+
*
82+
* @param {T} enumObject - The original object representing the enum-like mapping.
83+
* @param {Object} options - Configuration options.
84+
* @param {KeyName} [options.keyName="key"] - The name of the key to inject into each value.
85+
* @param {(enumObject: Readonly<HoistKeyToInner<T, KeyName>>) => TResult} options.tupleFactory - A factory function
86+
* that takes the modified enum object with keys hoisted and returns a tuple. It must include all keys from `enumObject`.
87+
*
88+
* This is required because typescript cannot convert from a union to a tuple with
89+
* * Guaranteed order
90+
* * Better performance than O(n^2)
91+
* See https://stackoverflow.com/questions/55127004/how-to-transform-union-type-to-tuple-type
92+
*
93+
* So, we have to provide a tuple factory. It constrains the tuple to make sure you're not missing any values.
94+
* Making our typescript compiler happy.
95+
*
96+
* @returns {void} This function does not return anything directly, but can be used to enforce compile-time constraints
97+
* and build strongly typed enums using TypeScript's type system.
98+
*
99+
* @throws {TypeError} If any object in `enumObject` already contains the `keyName` property, it will result in a type error.
100+
* @throws {Error} If the `tupleFactory` does not return a tuple that includes all keys, a compile-time type error will occur.
79101
*/
80102
export declare function ButterKeyedEnum<KeyName extends string = "key", const T extends {
81103
[K in keyof T]: KeyName extends keyof T[K] ? never : Record<string, any>;
82104
} = {
83105
[key: string]: any;
84-
}>(enumObject: T, options?: {
106+
}, TTuple extends [T[keyof T], ...T[keyof T][]] = [T[keyof T], ...T[keyof T][]], TResult extends [T[keyof T], ...T[keyof T][]] = TTuple>(enumObject: T, options: {
85107
keyName?: KeyName;
108+
/**
109+
* A factory function that takes the modified enum object with keys hoisted and returns a tuple.
110+
* It must include all keys from `enumObject`.
111+
*
112+
* @param enumObject The enum object with keys hoisted into each value
113+
* @returns A tuple of values from the enum object
114+
*
115+
* This is required because typescript cannot convert from a union to a tuple with
116+
* * Guaranteed order
117+
* * Better performance than O(n^2)
118+
* See https://stackoverflow.com/questions/55127004/how-to-transform-union-type-to-tuple-type
119+
*
120+
* So, we have to provide a tuple factory. It constrains the tuple to make sure you're not missing any values.
121+
* Making our typescript compiler happy.
122+
*/
123+
tupleFactory: (enumObject: Readonly<HoistKeyToInner<T, KeyName>>) => IsTypeEqual<TResult[number][KeyName], keyof T> extends true ? TResult : {
124+
error: "You must include all keys in the tuple";
125+
value: never;
126+
};
86127
}): {
87128
/**
88129
* The enum object
89130
*
90131
* @type {Readonly<TEnum>} The enum object with keys hoisted into each value
91132
*/
92133
readonly enum: Readonly<HoistKeyToInner<T, KeyName>>;
134+
/**
135+
* An ordered array of enum values as specified by the tupleFactory function
136+
*
137+
* @type {TTuple} The tuple of enum values in the order defined by tupleFactory
138+
*/
139+
readonly tuple: IsTypeEqual<TResult[number][KeyName], keyof T> extends true ? TResult : {
140+
error: "You must include all keys in the tuple";
141+
value: never;
142+
};
93143
/**
94144
* Gets a value by key
95145
*
@@ -137,4 +187,8 @@ type HoistKeyToInner<T, KeyName extends string = "key"> = {
137187
[P in keyof O | KeyName]: P extends keyof O ? O[P] : K;
138188
} : never;
139189
};
190+
/**
191+
* Utility type that checks if two types are equal
192+
*/
193+
type IsTypeEqual<A, B> = (<T>() => T extends A ? 1 : 2) extends (<T>() => T extends B ? 1 : 2) ? true : false;
140194
export {};

dist/index.js

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,12 +97,35 @@ function ButterTupleEnum(tuple) {
9797
* colorsEnum.keys // ['green']
9898
* ```
9999
*
100-
* @template TEnum The object type containing enum entries
101-
* @returns The keyed enum object with helper methods
100+
* @template KeyName - The name of the property to hoist the key into. Defaults to `"key"`.
101+
* @template T - The original enum-like object whose keys should not conflict with `keyName` in inner objects.
102+
* @template TTuple - The tuple of values derived from `T` that will represent the enum values.
103+
* @template TResult - The final result tuple validated against the keys of `T`.
104+
*
105+
* @param {T} enumObject - The original object representing the enum-like mapping.
106+
* @param {Object} options - Configuration options.
107+
* @param {KeyName} [options.keyName="key"] - The name of the key to inject into each value.
108+
* @param {(enumObject: Readonly<HoistKeyToInner<T, KeyName>>) => TResult} options.tupleFactory - A factory function
109+
* that takes the modified enum object with keys hoisted and returns a tuple. It must include all keys from `enumObject`.
110+
*
111+
* This is required because typescript cannot convert from a union to a tuple with
112+
* * Guaranteed order
113+
* * Better performance than O(n^2)
114+
* See https://stackoverflow.com/questions/55127004/how-to-transform-union-type-to-tuple-type
115+
*
116+
* So, we have to provide a tuple factory. It constrains the tuple to make sure you're not missing any values.
117+
* Making our typescript compiler happy.
118+
*
119+
* @returns {void} This function does not return anything directly, but can be used to enforce compile-time constraints
120+
* and build strongly typed enums using TypeScript's type system.
121+
*
122+
* @throws {TypeError} If any object in `enumObject` already contains the `keyName` property, it will result in a type error.
123+
* @throws {Error} If the `tupleFactory` does not return a tuple that includes all keys, a compile-time type error will occur.
102124
*/
103125
function ButterKeyedEnum(enumObject, options) {
104126
const $enum = (0, deep_freeze_es6_1.default)(Object.fromEntries(Object.entries(enumObject)
105127
.map(([key, value]) => [key, { ...value, [options?.keyName ?? "key"]: key }])));
128+
const $tuple = options.tupleFactory($enum);
106129
function getMany(keys) {
107130
return keys.map(key => $enum[key]);
108131
}
@@ -113,6 +136,12 @@ function ButterKeyedEnum(enumObject, options) {
113136
* @type {Readonly<TEnum>} The enum object with keys hoisted into each value
114137
*/
115138
enum: $enum,
139+
/**
140+
* An ordered array of enum values as specified by the tupleFactory function
141+
*
142+
* @type {TTuple} The tuple of enum values in the order defined by tupleFactory
143+
*/
144+
tuple: (0, deep_freeze_es6_1.default)($tuple),
116145
/**
117146
* Gets a value by key
118147
*

docs/index.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,17 @@ const test = ButterKeyedEnum({
6363
emoji: '🟩',
6464
hex: '#00FF00',
6565
},
66+
}, {
67+
// This is required because typescript cannot convert from a union to a tuple with
68+
// * Guaranteed order
69+
// * Better performance than O(n^2)
70+
// See https://stackoverflow.com/questions/55127004/how-to-transform-union-type-to-tuple-type
71+
//
72+
// So, we have to provide a tuple factory. It constrains the tuple to make sure you're not missing any values.
73+
// Making our typescript compiler happy.
74+
tupleFactory: (enumObject) => [
75+
enumObject.green,
76+
]
6677
})
6778

6879
console.log(test.enum.green)

docs/package-lock.json

Lines changed: 14 additions & 17 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,6 @@
99
"vitepress": "^1.6.3"
1010
},
1111
"dependencies": {
12-
"butter-enums": "^0.0.1"
12+
"butter-enums": "file:.."
1313
}
1414
}

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "butter-enums",
3-
"version": "0.0.2",
3+
"version": "0.0.3",
44
"description": "Typesafe specialized enums for TypeScript - Smooth like butter",
55
"main": "dist/index.js",
66
"types": "dist/index.d.ts",

0 commit comments

Comments
 (0)