claude-toolkit 0.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/LICENSE +21 -0
  3. package/README.md +126 -0
  4. package/bin/cli.ts +112 -0
  5. package/core/agents/ct-code-reviewer.md +123 -0
  6. package/core/agents/ct-github-workflow.md +137 -0
  7. package/core/commands/ct/code-quality.md +59 -0
  8. package/core/commands/ct/onboard.md +84 -0
  9. package/core/commands/ct/pr-review.md +104 -0
  10. package/core/commands/ct/pr-summary.md +59 -0
  11. package/core/commands/ct/proto-check.md +74 -0
  12. package/core/commands/ct/ticket.md +71 -0
  13. package/core/hooks/skill-eval.js +381 -0
  14. package/core/hooks/skill-eval.sh +35 -0
  15. package/core/hooks/skill-rules.schema.json +112 -0
  16. package/core/skills/systematic-debugging/SKILL.md +44 -0
  17. package/core/skills/testing-patterns/SKILL.md +52 -0
  18. package/core/skills/typescript-conventions/SKILL.md +57 -0
  19. package/core/skills/verification-before-completion/SKILL.md +42 -0
  20. package/docs/README.md +49 -0
  21. package/docs/agents/code-reviewer.md +76 -0
  22. package/docs/agents/github-workflow.md +98 -0
  23. package/docs/best-practices/solidjs/README.md +43 -0
  24. package/docs/best-practices/solidjs/anti-patterns.md +166 -0
  25. package/docs/best-practices/solidjs/component-patterns.md +131 -0
  26. package/docs/best-practices/solidjs/context-and-global-state.md +131 -0
  27. package/docs/best-practices/solidjs/control-flow.md +124 -0
  28. package/docs/best-practices/solidjs/data-fetching.md +205 -0
  29. package/docs/best-practices/solidjs/effects-and-lifecycle.md +113 -0
  30. package/docs/best-practices/solidjs/performance.md +100 -0
  31. package/docs/best-practices/solidjs/props-patterns.md +100 -0
  32. package/docs/best-practices/solidjs/reactivity-model.md +104 -0
  33. package/docs/best-practices/solidjs/signals-and-state.md +78 -0
  34. package/docs/best-practices/solidjs/stores-and-nested-state.md +111 -0
  35. package/docs/best-practices/solidjs/typescript-integration.md +186 -0
  36. package/docs/best-practices/typescript/README.md +45 -0
  37. package/docs/best-practices/typescript/any-and-unknown.md +73 -0
  38. package/docs/best-practices/typescript/deriving-vs-decoupling.md +83 -0
  39. package/docs/best-practices/typescript/discriminated-unions.md +75 -0
  40. package/docs/best-practices/typescript/enums-alternatives.md +72 -0
  41. package/docs/best-practices/typescript/essential-patterns.md +119 -0
  42. package/docs/best-practices/typescript/generics-patterns.md +105 -0
  43. package/docs/best-practices/typescript/micro-opinions.md +87 -0
  44. package/docs/best-practices/typescript/runtime-validation.md +62 -0
  45. package/docs/best-practices/typescript/satisfies-operator.md +100 -0
  46. package/docs/best-practices/typescript/tsconfig-cheat-sheet.md +129 -0
  47. package/docs/best-practices/typescript/type-organization.md +64 -0
  48. package/docs/best-practices/typescript/type-vs-interface.md +80 -0
  49. package/docs/commands/code-quality.md +42 -0
  50. package/docs/commands/onboard.md +72 -0
  51. package/docs/commands/pr-review.md +102 -0
  52. package/docs/commands/pr-summary.md +50 -0
  53. package/docs/commands/proto-check.md +59 -0
  54. package/docs/commands/ticket.md +56 -0
  55. package/docs/skills/systematic-debugging.md +70 -0
  56. package/docs/skills/testing-patterns.md +89 -0
  57. package/docs/skills/typescript-conventions.md +137 -0
  58. package/docs/skills/verification-before-completion.md +91 -0
  59. package/docs/stacks/cloudflare-d1-kv.md +110 -0
  60. package/docs/stacks/i18n-typesafe.md +141 -0
  61. package/docs/stacks/protobuf-contracts.md +85 -0
  62. package/docs/stacks/rust-wasm-patterns.md +106 -0
  63. package/docs/stacks/solidjs-patterns.md +110 -0
  64. package/docs/stacks/vanilla-extract-patterns.md +115 -0
  65. package/package.json +58 -0
  66. package/src/generator.ts +317 -0
  67. package/src/index.ts +30 -0
  68. package/src/types.ts +85 -0
  69. package/src/utils.ts +53 -0
  70. package/stacks/cloudflare/skills/cloudflare-d1-kv/SKILL.md +84 -0
  71. package/stacks/cloudflare/stack.json +26 -0
  72. package/stacks/i18n-typesafe/skills/i18n-typesafe/SKILL.md +64 -0
  73. package/stacks/i18n-typesafe/stack.json +25 -0
  74. package/stacks/protobuf/skills/protobuf-contracts/SKILL.md +78 -0
  75. package/stacks/protobuf/stack.json +25 -0
  76. package/stacks/rust-wasm/skills/rust-wasm-patterns/SKILL.md +76 -0
  77. package/stacks/rust-wasm/stack.json +26 -0
  78. package/stacks/solidjs/skills/solidjs-patterns/SKILL.md +66 -0
  79. package/stacks/solidjs/stack.json +52 -0
  80. package/stacks/vanilla-extract/skills/vanilla-extract-patterns/SKILL.md +76 -0
  81. package/stacks/vanilla-extract/stack.json +40 -0
  82. package/templates/claude-toolkit.config.ts +34 -0
  83. package/templates/configs/biome.base.json +35 -0
  84. package/templates/configs/tsconfig.base.json +16 -0
  85. package/templates/skill-rules.base.json +98 -0
@@ -0,0 +1,111 @@
1
+ # Stores & Nested State
2
+
3
+ > Sources: [Stores](https://docs.solidjs.com/concepts/stores), [Complex State Management](https://docs.solidjs.com/guides/complex-state-management) — SolidJS Docs
4
+
5
+ ## When to Use Stores
6
+
7
+ Use `createStore` when your state is a **complex, nested object** or an **array of objects** that receives granular updates. Stores provide fine-grained reactivity at the **property level** — updating a nested property only re-renders the parts of UI that depend on that specific property.
8
+
9
+ ```typescript
10
+ import { createStore } from "solid-js/store";
11
+
12
+ const [state, setState] = createStore({
13
+ user: { name: "Alice", settings: { theme: "dark" } },
14
+ tasks: [
15
+ { id: 1, text: "Learn Solid", completed: false },
16
+ ],
17
+ });
18
+ ```
19
+
20
+ **Reading:** Access properties directly (no function call needed):
21
+
22
+ ```typescript
23
+ <span>{state.user.name}</span>
24
+ <span>{state.user.settings.theme}</span>
25
+ ```
26
+
27
+ ## Path Syntax Updates
28
+
29
+ Stores use a **path syntax** for targeted, efficient updates:
30
+
31
+ ```typescript
32
+ // Update a nested property
33
+ setState("user", "settings", "theme", "light");
34
+
35
+ // Update a specific array item
36
+ setState("tasks", 0, "completed", true);
37
+
38
+ // Update with a function
39
+ setState("tasks", 0, "completed", prev => !prev);
40
+ ```
41
+
42
+ ### Array Operations
43
+
44
+ ```typescript
45
+ // Append to array (targeted — doesn't recreate the array)
46
+ setState("tasks", state.tasks.length, {
47
+ id: 2, text: "Ship it", completed: false,
48
+ });
49
+
50
+ // Update multiple indices
51
+ setState("tasks", [0, 2], "completed", true);
52
+
53
+ // Update by range
54
+ setState("tasks", { from: 0, to: 3 }, "completed", false);
55
+
56
+ // Update by predicate
57
+ setState("tasks", task => task.completed, "archived", true);
58
+ ```
59
+
60
+ ## `produce` for Imperative Updates
61
+
62
+ When you need to modify multiple properties at once, `produce` provides an imperative API:
63
+
64
+ ```typescript
65
+ import { produce } from "solid-js/store";
66
+
67
+ setState("tasks", 0, produce(task => {
68
+ task.completed = true;
69
+ task.completedAt = Date.now();
70
+ }));
71
+
72
+ // Also works at the store root
73
+ setState(produce(state => {
74
+ state.tasks.push({ id: 3, text: "New task", completed: false });
75
+ state.user.name = "Bob";
76
+ }));
77
+ ```
78
+
79
+ `produce` creates a draft, applies your mutations, then produces an immutable update. Only use it with objects and arrays (not Sets or Maps).
80
+
81
+ ## `reconcile` for External Data
82
+
83
+ When you receive a whole new object (e.g., from an API response), `reconcile` diffs the old and new data, only triggering updates for properties that actually changed:
84
+
85
+ ```typescript
86
+ import { reconcile } from "solid-js/store";
87
+
88
+ const newData = await fetchTasks();
89
+ setState("tasks", reconcile(newData));
90
+ ```
91
+
92
+ This avoids re-rendering everything when most of the data hasn't changed.
93
+
94
+ ## `unwrap` for Snapshots
95
+
96
+ Extract a plain JavaScript object from a store (strips the reactive proxy):
97
+
98
+ ```typescript
99
+ import { unwrap } from "solid-js/store";
100
+
101
+ const snapshot = unwrap(state);
102
+ // Use snapshot for third-party libraries, logging, or serialization
103
+ ```
104
+
105
+ ## Best Practices
106
+
107
+ - **Use path syntax** for targeted updates — it's more efficient than replacing objects
108
+ - **Use `produce`** when modifying multiple properties in one operation
109
+ - **Use `reconcile`** when integrating API responses to avoid unnecessary updates
110
+ - **Don't mutate store properties directly** — always go through `setState`
111
+ - **Stores are batched automatically** — multiple `setState` calls in the same synchronous block are batched into one update
@@ -0,0 +1,186 @@
1
+ # TypeScript Integration
2
+
3
+ > Source: [TypeScript](https://docs.solidjs.com/configuration/typescript) — SolidJS Docs
4
+
5
+ ## TSConfig for SolidJS
6
+
7
+ Required settings for Solid's JSX transformation:
8
+
9
+ ```jsonc
10
+ {
11
+ "compilerOptions": {
12
+ "jsx": "preserve",
13
+ "jsxImportSource": "solid-js",
14
+ "strict": true,
15
+ "noEmit": true,
16
+ "isolatedModules": true
17
+ }
18
+ }
19
+ ```
20
+
21
+ `"jsx": "preserve"` is required — Solid's JSX transformation is incompatible with TypeScript's built-in JSX handling.
22
+
23
+ ## Typing Signals
24
+
25
+ ```typescript
26
+ // Type is inferred from the initial value
27
+ const [count, setCount] = createSignal(0); // Signal<number>
28
+
29
+ // Explicit type when initial value doesn't capture the full type
30
+ const [user, setUser] = createSignal<User | null>(null);
31
+
32
+ // Without a default, type includes undefined
33
+ const [data, setData] = createSignal<string>(); // Accessor<string | undefined>
34
+ ```
35
+
36
+ ## Typing Stores
37
+
38
+ ```typescript
39
+ type AppState = {
40
+ user: User;
41
+ tasks: Task[];
42
+ settings: { theme: "light" | "dark" };
43
+ };
44
+
45
+ const [state, setState] = createStore<AppState>({
46
+ user: { id: "1", name: "Alice" },
47
+ tasks: [],
48
+ settings: { theme: "dark" },
49
+ });
50
+ ```
51
+
52
+ ## Typing Context
53
+
54
+ Provide a type parameter to `createContext`:
55
+
56
+ ```typescript
57
+ type AuthContext = {
58
+ user: User | null;
59
+ login: (credentials: Credentials) => Promise<void>;
60
+ logout: () => void;
61
+ };
62
+
63
+ const AuthContext = createContext<AuthContext>();
64
+ ```
65
+
66
+ **Factory pattern** — derive the type from the factory function:
67
+
68
+ ```typescript
69
+ function createAuthStore() { /* ... */ }
70
+
71
+ const AuthContext = createContext<ReturnType<typeof createAuthStore>>();
72
+ ```
73
+
74
+ ## Component Types
75
+
76
+ ```typescript
77
+ import type { Component, ParentComponent, VoidComponent, FlowComponent } from "solid-js";
78
+
79
+ // Standard component
80
+ const App: Component = () => <div>Hello</div>;
81
+
82
+ // Accepts children
83
+ const Card: ParentComponent<{ title: string }> = (props) => (
84
+ <div>
85
+ <h2>{props.title}</h2>
86
+ {props.children}
87
+ </div>
88
+ );
89
+
90
+ // No children allowed
91
+ const Icon: VoidComponent<{ name: string }> = (props) => (
92
+ <svg class={`icon-${props.name}`} />
93
+ );
94
+ ```
95
+
96
+ ### Generic Components
97
+
98
+ `Component` types don't support generics. Use function declarations:
99
+
100
+ ```typescript
101
+ function Select<T>(props: {
102
+ options: T[];
103
+ value: T;
104
+ onChange: (value: T) => void;
105
+ label: (item: T) => string;
106
+ }): JSX.Element {
107
+ return (
108
+ <For each={props.options}>
109
+ {(option) => (
110
+ <button
111
+ classList={{ active: option === props.value }}
112
+ onClick={() => props.onChange(option)}
113
+ >
114
+ {props.label(option)}
115
+ </button>
116
+ )}
117
+ </For>
118
+ );
119
+ }
120
+ ```
121
+
122
+ ## Event Handlers
123
+
124
+ Inline handlers are auto-typed. For extracted handlers:
125
+
126
+ ```typescript
127
+ const handleInput: JSX.EventHandler<HTMLInputElement, InputEvent> = (e) => {
128
+ // e.currentTarget is typed as HTMLInputElement
129
+ setValue(e.currentTarget.value);
130
+ };
131
+ ```
132
+
133
+ ## Control Flow Type Narrowing
134
+
135
+ TypeScript can't narrow types through accessors. Use the callback form of `<Show>`:
136
+
137
+ ```typescript
138
+ // WRONG — TypeScript doesn't know user() is non-null inside Show
139
+ <Show when={user()}>
140
+ <span>{user()!.name}</span> {/* Forced to use ! */}
141
+ </Show>
142
+
143
+ // RIGHT — callback provides narrowed accessor
144
+ <Show when={user()}>
145
+ {(u) => <span>{u().name}</span>}
146
+ </Show>
147
+ ```
148
+
149
+ ## Extending JSX Types
150
+
151
+ ### Custom Events
152
+
153
+ ```typescript
154
+ declare module "solid-js" {
155
+ namespace JSX {
156
+ interface CustomEvents {
157
+ myEvent: CustomEvent<{ detail: string }>;
158
+ }
159
+ }
160
+ }
161
+ ```
162
+
163
+ ### Custom Directives
164
+
165
+ ```typescript
166
+ declare module "solid-js" {
167
+ namespace JSX {
168
+ interface Directives {
169
+ tooltip: string;
170
+ clickOutside: () => void;
171
+ }
172
+ }
173
+ }
174
+ ```
175
+
176
+ ### Explicit Properties/Attributes
177
+
178
+ ```typescript
179
+ declare module "solid-js" {
180
+ namespace JSX {
181
+ interface ExplicitProperties { /* prop:___ */ }
182
+ interface ExplicitAttributes { /* attr:___ */ }
183
+ interface ExplicitBoolAttributes { /* bool:___ */ }
184
+ }
185
+ }
186
+ ```
@@ -0,0 +1,45 @@
1
+ # Matt Pocock's TypeScript Best Practices
2
+
3
+ > A curated collection of TypeScript best practices, opinions, and patterns from [Matt Pocock](https://www.mattpocock.com/) and [Total TypeScript](https://www.totaltypescript.com/).
4
+
5
+ Matt Pocock is one of the most influential TypeScript educators. These documents capture his opinionated, production-tested guidance — not generic advice, but specific stances backed by reasoning.
6
+
7
+ ## Core Choices
8
+
9
+ Foundational decisions that shape every TypeScript project.
10
+
11
+ | Guide | Summary |
12
+ |---|---|
13
+ | [Type vs Interface](type-vs-interface.md) | Default to `type`. Use `interface` only for `extends`. |
14
+ | [Enums & Alternatives](enums-alternatives.md) | Avoid enums. Use `as const` objects with derived unions. |
15
+ | [any & unknown](any-and-unknown.md) | Ban `any` by default. Know the two legitimate exceptions. |
16
+ | [TSConfig Cheat Sheet](tsconfig-cheat-sheet.md) | Recommended compiler options for every project shape. |
17
+
18
+ ## Patterns
19
+
20
+ Type-level patterns that make code safer and more expressive.
21
+
22
+ | Guide | Summary |
23
+ |---|---|
24
+ | [Discriminated Unions](discriminated-unions.md) | Model states as unions — make illegal states unrepresentable. |
25
+ | [Generics Patterns](generics-patterns.md) | Three patterns: types-to-types, types-to-functions, inference. |
26
+ | [The `satisfies` Operator](satisfies-operator.md) | Five practical uses for type validation without losing inference. |
27
+ | [Essential Patterns](essential-patterns.md) | Branded types, globals, assertion functions, classes. |
28
+
29
+ ## Architecture
30
+
31
+ How to organize and connect types across a codebase.
32
+
33
+ | Guide | Summary |
34
+ |---|---|
35
+ | [Type Organization](type-organization.md) | Colocate, share at folder level, or extract to a package. |
36
+ | [Deriving vs Decoupling](deriving-vs-decoupling.md) | Derive when tightly coupled. Decouple across concerns. |
37
+ | [Runtime Validation](runtime-validation.md) | Use Zod at trust boundaries. Skip it for controlled inputs. |
38
+
39
+ ## Micro-Opinions
40
+
41
+ Small but specific stances on everyday TypeScript choices.
42
+
43
+ | Guide | Summary |
44
+ |---|---|
45
+ | [Micro-Opinions](micro-opinions.md) | Return types, `const` vs `let`, `T[]` vs `Array<T>`, method shorthand, `Function` type. |
@@ -0,0 +1,73 @@
1
+ # `any` & `unknown`
2
+
3
+ > Source: [any Considered Harmful, Except For These Cases](https://www.totaltypescript.com/any-considered-harmful) — Matt Pocock
4
+
5
+ ## The Rule
6
+
7
+ **Ban `any` from your codebase.** Enable the ESLint rule that prevents its use and avoid it wherever possible.
8
+
9
+ `any` disables TypeScript's core value proposition: type checking, autocomplete, and safety guarantees. It's a contagion — once `any` enters an expression, it spreads through assignments and return values.
10
+
11
+ ## What to Use Instead
12
+
13
+ | Situation | Use |
14
+ |---|---|
15
+ | Type is genuinely unknown | `unknown` (then narrow before use) |
16
+ | Type varies but has a consistent shape | Generics with constraints |
17
+ | You know the data shape | The specific type |
18
+
19
+ ```typescript
20
+ // Instead of any
21
+ function parse(input: unknown): User {
22
+ if (typeof input === "object" && input !== null && "name" in input) {
23
+ return input as User;
24
+ }
25
+ throw new Error("Invalid input");
26
+ }
27
+ ```
28
+
29
+ ## The Two Legitimate Exceptions
30
+
31
+ Matt identifies two advanced scenarios where `any` is the right choice.
32
+
33
+ ### Exception 1: Type Argument Constraints in Generics
34
+
35
+ When building generic utility types, `unknown` in constraints can be too restrictive:
36
+
37
+ ```typescript
38
+ // This is too restrictive — only works with specific function shapes
39
+ type ReturnType<T extends (...args: unknown[]) => unknown> = ...
40
+
41
+ // any[] constraint is correct — "we don't care what the function accepts"
42
+ type ReturnType<T extends (...args: any[]) => any> = ...
43
+ ```
44
+
45
+ Using `any[]` here is intentional: you're declaring that the constraint should accept **any function**, not that you're bypassing safety.
46
+
47
+ ### Exception 2: Conditional Types in Generic Functions
48
+
49
+ TypeScript's type narrowing sometimes can't verify that conditional type logic matches runtime behavior:
50
+
51
+ ```typescript
52
+ function processValue<T extends string | number>(
53
+ value: T
54
+ ): T extends string ? string[] : number {
55
+ if (typeof value === "string") {
56
+ return value.split("") as any; // TypeScript can't narrow conditional return types
57
+ }
58
+ return (value as number) * 2 as any;
59
+ }
60
+ ```
61
+
62
+ In these cases, Matt recommends using `as any` **and adding a unit test** to validate the function's behavior at runtime.
63
+
64
+ ## The Pragmatic Approach
65
+
66
+ When you genuinely need `any`, use an `eslint-disable` comment to make it explicit and reviewable:
67
+
68
+ ```typescript
69
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
70
+ type Callback = (...args: any[]) => void;
71
+ ```
72
+
73
+ This creates a searchable record of every `any` in your codebase, making them easy to audit and justify in code review.
@@ -0,0 +1,83 @@
1
+ # Deriving vs Decoupling
2
+
3
+ > Source: [Deriving vs Decoupling: When NOT To Be A TypeScript Wizard](https://www.totaltypescript.com/deriving-vs-decoupling) — Matt Pocock
4
+
5
+ ## The Distinction
6
+
7
+ - **Deriving** = creating a type that depends on another type's structure (`Pick`, `Omit`, `typeof`, mapped types)
8
+ - **Decoupling** = creating independent types with no dependency between them
9
+
10
+ The temptation to derive comes from TypeScript's powerful type manipulation. But cleverness isn't always correctness.
11
+
12
+ ## When to Decouple
13
+
14
+ Decouple when types serve **different concerns** — different responsibilities with different reasons to change.
15
+
16
+ ```typescript
17
+ // Database layer
18
+ type User = {
19
+ id: string;
20
+ name: string;
21
+ email: string;
22
+ imageUrl: string;
23
+ passwordHash: string;
24
+ };
25
+
26
+ // UI layer — DON'T derive from User
27
+ type AvatarProps = {
28
+ imageUrl: string;
29
+ name: string;
30
+ };
31
+ ```
32
+
33
+ Why not `Pick<User, "imageUrl" | "name">`? Because:
34
+ - `User` lives in database code; `AvatarProps` is UI logic
35
+ - They have different reasons to change
36
+ - If `User.imageUrl` is renamed to `User.avatarUrl`, should every UI component break? Probably not.
37
+ - The component should remain portable even if the data model moves
38
+
39
+ As Matt puts it: "It's smarter to do the simple thing."
40
+
41
+ ## When to Derive
42
+
43
+ Derive when types share a **common concern** and are tightly coupled by design.
44
+
45
+ ### From `as const` objects
46
+
47
+ ```typescript
48
+ const albumTypes = {
49
+ CD: "cd",
50
+ VINYL: "vinyl",
51
+ DIGITAL: "digital",
52
+ } as const;
53
+
54
+ type AlbumType = (typeof albumTypes)[keyof typeof albumTypes];
55
+ // "cd" | "vinyl" | "digital"
56
+ ```
57
+
58
+ Maintaining a separate union and object would mean updating two places for every change — pure busywork.
59
+
60
+ ### Related variants
61
+
62
+ ```typescript
63
+ type User = {
64
+ id: string;
65
+ name: string;
66
+ email: string;
67
+ };
68
+
69
+ type UserWithoutId = Omit<User, "id">;
70
+ ```
71
+
72
+ These are directly related — a `UserWithoutId` is meaningless outside the context of `User`.
73
+
74
+ ## Decision Framework
75
+
76
+ > "The decision to derive or decouple is all about reducing your future workload."
77
+
78
+ | Signal | Action |
79
+ |---|---|
80
+ | Changing one type should always change the other | **Derive** |
81
+ | Types serve different layers or concerns | **Decouple** |
82
+ | Derivation adds complexity for marginal DRY benefit | **Decouple** |
83
+ | Types share the same source of truth | **Derive** |
@@ -0,0 +1,75 @@
1
+ # Discriminated Unions
2
+
3
+ > Source: [TypeScript Discriminated Unions for Frontend Developers](https://www.totaltypescript.com/discriminated-unions-are-a-devs-best-friend) — Matt Pocock
4
+
5
+ ## The Problem: Bag of Optionals
6
+
7
+ The naive approach to modeling state uses optional properties in a single type:
8
+
9
+ ```typescript
10
+ // Don't do this
11
+ type State = {
12
+ status: "loading" | "success" | "error";
13
+ data?: { id: string };
14
+ error?: Error;
15
+ };
16
+ ```
17
+
18
+ This allows **impossible states**: you can have `status: "success"` with no `data`, or `status: "loading"` with an `error`. TypeScript can't help you because every property is independently optional.
19
+
20
+ ## The Solution: Discriminated Unions
21
+
22
+ Model each state as a distinct member of a union, with only the properties relevant to that state:
23
+
24
+ ```typescript
25
+ type State =
26
+ | { status: "loading" }
27
+ | { status: "success"; data: { id: string } }
28
+ | { status: "error"; error: Error };
29
+ ```
30
+
31
+ Now `data` only exists when `status` is `"success"`, and `error` only exists when `status` is `"error"`. Impossible states are unrepresentable.
32
+
33
+ ## Type Narrowing
34
+
35
+ You must check the discriminator before accessing state-specific properties. This strictness is a feature:
36
+
37
+ ```typescript
38
+ function handleState(state: State) {
39
+ if (state.status === "success") {
40
+ // TypeScript knows state.data exists here
41
+ console.log(state.data.id);
42
+ }
43
+
44
+ if (state.status === "error") {
45
+ // TypeScript knows state.error exists here
46
+ console.log(state.error.message);
47
+ }
48
+ }
49
+ ```
50
+
51
+ ## Component Props Pattern
52
+
53
+ Apply discriminated unions to component APIs with conditional properties:
54
+
55
+ ```typescript
56
+ type ModalProps =
57
+ | {
58
+ variant: "with-description";
59
+ title: string;
60
+ description: string;
61
+ buttonText: string;
62
+ }
63
+ | {
64
+ variant: "base";
65
+ title: string;
66
+ };
67
+ ```
68
+
69
+ This prevents consumers from passing `description` without `buttonText`, or using the wrong combination of props.
70
+
71
+ ## Why This Matters
72
+
73
+ As Matt puts it: "Instead of a big optional bag of data, you'll start understanding the connections between data and UI."
74
+
75
+ Discriminated unions force you to think about which data belongs together in which state — and the compiler enforces those relationships for you.
@@ -0,0 +1,72 @@
1
+ # Enums & Alternatives
2
+
3
+ > Source: [Why I Don't Like Enums](https://www.totaltypescript.com/why-i-dont-like-typescript-enums) — Matt Pocock
4
+
5
+ ## The Verdict
6
+
7
+ **Avoid enums. Use `as const` objects with derived union types instead.**
8
+
9
+ ## Why Enums Are Problematic
10
+
11
+ ### 1. Numeric Enums Accept Raw Numbers
12
+
13
+ Numeric enums allow passing any raw number where an enum value is expected — completely defeating the purpose of type safety.
14
+
15
+ ```typescript
16
+ enum Direction {
17
+ Up, // 0
18
+ Down, // 1
19
+ Left, // 2
20
+ Right, // 3
21
+ }
22
+
23
+ function move(dir: Direction) {}
24
+
25
+ move(99); // No error! TypeScript allows this.
26
+ ```
27
+
28
+ ### 2. Inconsistent Behavior
29
+
30
+ Numeric and string enums behave differently:
31
+ - Numeric enums generate **bidirectional** mappings (value-to-key and key-to-value), doubling the runtime keys
32
+ - String enums enforce stricter type-checking but don't generate reverse mappings
33
+ - You can mix numeric and string values in a single enum (don't)
34
+
35
+ ### 3. Nominal Typing
36
+
37
+ Enums use nominal typing — two enums with identical values are not interchangeable. This breaks from JavaScript's structural approach and creates surprising incompatibilities.
38
+
39
+ ### 4. Implementation Bugs
40
+
41
+ The TypeScript repository has 71 open enum-related bug issues. The TS team has indicated many are architecturally unfixable.
42
+
43
+ ## The Alternative: `as const` Objects
44
+
45
+ ```typescript
46
+ const Direction = {
47
+ Up: "up",
48
+ Down: "down",
49
+ Left: "left",
50
+ Right: "right",
51
+ } as const;
52
+
53
+ type Direction = (typeof Direction)[keyof typeof Direction];
54
+ // "up" | "down" | "left" | "right"
55
+ ```
56
+
57
+ This gives you:
58
+ - A runtime object for lookups (`Direction.Up`)
59
+ - A union type for type safety (`Direction`)
60
+ - No surprising behavior
61
+ - Standard JavaScript semantics
62
+
63
+ ## If You Must Use Enums
64
+
65
+ Use **string enums only**. They behave more predictably, resemble the transpiled output more closely, and don't allow raw value assignment.
66
+
67
+ ```typescript
68
+ enum Status {
69
+ Active = "active",
70
+ Inactive = "inactive",
71
+ }
72
+ ```