Convert a string tuple into a readonly enum-like object: PascalCase keys, filtered array keys, and index strings turned back into number literals.
TypeScript enums compile down to plain objects, and one mapped type is enough to build that object shape from a string tuple yourself.
Enum<T, N> converts a string tuple into an object type that behaves like a TypeScript enum. It packs a lot of mapped-type technique into one declaration: you iterate a tuple's numeric indices, filter out its array machinery with key remapping, rename keys with Capitalize, and for the numeric mode convert index strings like '2' back into number literals with infer ... extends.
enum is a TypeScript-only construct. JavaScript has no such syntax, so the compiler transpiles every enum into a plain object at runtime:
let OperatingSystem;
(function (OperatingSystem) {
OperatingSystem[OperatingSystem["MacOS"] = 0] = "MacOS";
OperatingSystem[OperatingSystem["Windows"] = 1] = "Windows";
OperatingSystem[OperatingSystem["Linux"] = 2] = "Linux";
})(OperatingSystem || (OperatingSystem = {}));That runtime object is exactly the shape you're modeling here: given a string tuple, produce an object type that behaves like such an enum, with each property name converted to PascalCase.
Enum<["macOS", "Windows", "Linux"]>
// -> { readonly MacOS: "macOS", readonly Windows: "Windows", readonly Linux: "Linux" }If true is given in the second argument, the value should be a number literal.
Enum<["macOS", "Windows", "Linux"], true>
// -> { readonly MacOS: 0, readonly Windows: 1, readonly Linux: 2 }The enum is an original syntax of TypeScript (it does not exist in JavaScript). So it is converted to like the following form as a result of transpilation:
let OperatingSystem;
(function (OperatingSystem) {
OperatingSystem[OperatingSystem["MacOS"] = 0] = "MacOS";
OperatingSystem[OperatingSystem["Windows"] = 1] = "Windows";
OperatingSystem[OperatingSystem["Linux"] = 2] = "Linux";
})(OperatingSystem || (OperatingSystem = {}));In this question, the type should convert a given string tuple to an object that behaves like an enum. Moreover, the property of an enum is preferably a pascal case.
Enum<["macOS", "Windows", "Linux"]>
// -> { readonly MacOS: "macOS", readonly Windows: "Windows", readonly Linux: "Linux" }If true is given in the second argument, the value should be a number literal.
Enum<["macOS", "Windows", "Linux"], true>
// -> { readonly MacOS: 0, readonly Windows: 1, readonly Linux: 2 }View on GitHub: https://tsch.js.org/472
Change the following code to make the test cases pass (no type check errors).
π Lifetime-License is leaving on August 10, 2026
Get it now for $29
One-time payment. Lifetime access to all pro challenges.
The whole solution is one mapped type:
type Enum<T extends readonly string[], N extends boolean = false> = {
readonly [K in keyof T as K extends `${number}`
? Capitalize<T[K]>
: never]: N extends true
? K extends `${infer Index extends number}`
? Index
: never
: T[K]
}Every clause in it solves a distinct sub-problem, so it pays to go through them one at a time.
When T is a tuple, keyof T is not just 0 | 1 | 2. It includes everything an array carries around: 'length', 'map', 'push', plus the element indices as strings:
// keyof readonly ['macOS', 'Windows', 'Linux'] includes:
// '0' | '1' | '2' | 'length' | 'toString' | 'map' | ...A plain { [K in keyof T]: ... } would actually be harmless: mapped types over arrays and tuples get special homomorphic treatment, where only the elements are mapped and the result is another tuple. But the moment you add an as remapping clause (which we need to rename the keys), that special case is gone, and the mapped type really does iterate all of keyof T, 'length' and methods included. So we need a filter.
as ... : neverKey remapping does more than rename: remap a key to never and it is dropped from the result. The clause
[object Object]does two jobs at once. The template literal `${number}` matches exactly the strings that look like numbers, so '0', '1', '2' pass through while 'length' and every method name gets remapped to never and vanishes. This filter is also why Enum<[]> correctly produces {}: an empty tuple has no numeric keys, so nothing survives.
CapitalizeFor the surviving index keys, the new key is Capitalize<T[K]>: an indexed access into the tuple (T['0'] is 'macOS') fed through the intrinsic Capitalize utility, which uppercases only the first character:
// K = '0' β T[K] = 'macOS' β Capitalize<'macOS'> = 'MacOS'
// K = '1' β T[K] = 'Windows' β 'Windows' (already capitalized)Note this is exactly "preferably pascal case" as the tests define it: 'xargs' becomes 'Xargs', not 'XArgs'. Only the first letter changes.
The value side switches on the second type parameter, which defaults to false:
N extends true
? K extends `${infer Index extends number}` ? Index : never
: T[K]N = false): the value is T[K], the original tuple element: { readonly MacOS: 'macOS', ... }.N = true): we want the index as a number literal. But K is the string '2', not the number 2. The pattern `${infer Index extends number}` uses a TypeScript 4.8 feature: constraining an infer inside a template literal makes the compiler re-parse the matched text as that type. So '2' infers Index = 2, a genuine number literal, which is what Enum<typeof Command, true> needs to produce Shift: 9.Without infer ... extends number you'd be stuck: '9' and 9 are different literal types, and no amount of plain template-literal matching converts one to the other.
The readonly modifier before [K in ...] marks every generated property read-only, matching how real enum objects can't be reassigned. And the constraint T extends readonly string[] (note the readonly) is required because the tests pass arrays declared with as const, which are readonly tuples. A plain string[] constraint would reject them.
Enum<[]> β {}: no indices survive the `${number}` filter.'Windows', 'Linux') pass through Capitalize unchanged.'9' becomes the literal 9 with no arithmetic tricks needed.Mapping a tuple's own indices and pruning keys via as ... never are moves you'll reuse constantly in advanced type-level code, and the infer ... extends round-trip shows up wherever string indices need to become numbers again.
This challenge is originally from here.