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.
- package/CHANGELOG.md +38 -0
- package/LICENSE +21 -0
- package/README.md +126 -0
- package/bin/cli.ts +112 -0
- package/core/agents/ct-code-reviewer.md +123 -0
- package/core/agents/ct-github-workflow.md +137 -0
- package/core/commands/ct/code-quality.md +59 -0
- package/core/commands/ct/onboard.md +84 -0
- package/core/commands/ct/pr-review.md +104 -0
- package/core/commands/ct/pr-summary.md +59 -0
- package/core/commands/ct/proto-check.md +74 -0
- package/core/commands/ct/ticket.md +71 -0
- package/core/hooks/skill-eval.js +381 -0
- package/core/hooks/skill-eval.sh +35 -0
- package/core/hooks/skill-rules.schema.json +112 -0
- package/core/skills/systematic-debugging/SKILL.md +44 -0
- package/core/skills/testing-patterns/SKILL.md +52 -0
- package/core/skills/typescript-conventions/SKILL.md +57 -0
- package/core/skills/verification-before-completion/SKILL.md +42 -0
- package/docs/README.md +49 -0
- package/docs/agents/code-reviewer.md +76 -0
- package/docs/agents/github-workflow.md +98 -0
- package/docs/best-practices/solidjs/README.md +43 -0
- package/docs/best-practices/solidjs/anti-patterns.md +166 -0
- package/docs/best-practices/solidjs/component-patterns.md +131 -0
- package/docs/best-practices/solidjs/context-and-global-state.md +131 -0
- package/docs/best-practices/solidjs/control-flow.md +124 -0
- package/docs/best-practices/solidjs/data-fetching.md +205 -0
- package/docs/best-practices/solidjs/effects-and-lifecycle.md +113 -0
- package/docs/best-practices/solidjs/performance.md +100 -0
- package/docs/best-practices/solidjs/props-patterns.md +100 -0
- package/docs/best-practices/solidjs/reactivity-model.md +104 -0
- package/docs/best-practices/solidjs/signals-and-state.md +78 -0
- package/docs/best-practices/solidjs/stores-and-nested-state.md +111 -0
- package/docs/best-practices/solidjs/typescript-integration.md +186 -0
- package/docs/best-practices/typescript/README.md +45 -0
- package/docs/best-practices/typescript/any-and-unknown.md +73 -0
- package/docs/best-practices/typescript/deriving-vs-decoupling.md +83 -0
- package/docs/best-practices/typescript/discriminated-unions.md +75 -0
- package/docs/best-practices/typescript/enums-alternatives.md +72 -0
- package/docs/best-practices/typescript/essential-patterns.md +119 -0
- package/docs/best-practices/typescript/generics-patterns.md +105 -0
- package/docs/best-practices/typescript/micro-opinions.md +87 -0
- package/docs/best-practices/typescript/runtime-validation.md +62 -0
- package/docs/best-practices/typescript/satisfies-operator.md +100 -0
- package/docs/best-practices/typescript/tsconfig-cheat-sheet.md +129 -0
- package/docs/best-practices/typescript/type-organization.md +64 -0
- package/docs/best-practices/typescript/type-vs-interface.md +80 -0
- package/docs/commands/code-quality.md +42 -0
- package/docs/commands/onboard.md +72 -0
- package/docs/commands/pr-review.md +102 -0
- package/docs/commands/pr-summary.md +50 -0
- package/docs/commands/proto-check.md +59 -0
- package/docs/commands/ticket.md +56 -0
- package/docs/skills/systematic-debugging.md +70 -0
- package/docs/skills/testing-patterns.md +89 -0
- package/docs/skills/typescript-conventions.md +137 -0
- package/docs/skills/verification-before-completion.md +91 -0
- package/docs/stacks/cloudflare-d1-kv.md +110 -0
- package/docs/stacks/i18n-typesafe.md +141 -0
- package/docs/stacks/protobuf-contracts.md +85 -0
- package/docs/stacks/rust-wasm-patterns.md +106 -0
- package/docs/stacks/solidjs-patterns.md +110 -0
- package/docs/stacks/vanilla-extract-patterns.md +115 -0
- package/package.json +58 -0
- package/src/generator.ts +317 -0
- package/src/index.ts +30 -0
- package/src/types.ts +85 -0
- package/src/utils.ts +53 -0
- package/stacks/cloudflare/skills/cloudflare-d1-kv/SKILL.md +84 -0
- package/stacks/cloudflare/stack.json +26 -0
- package/stacks/i18n-typesafe/skills/i18n-typesafe/SKILL.md +64 -0
- package/stacks/i18n-typesafe/stack.json +25 -0
- package/stacks/protobuf/skills/protobuf-contracts/SKILL.md +78 -0
- package/stacks/protobuf/stack.json +25 -0
- package/stacks/rust-wasm/skills/rust-wasm-patterns/SKILL.md +76 -0
- package/stacks/rust-wasm/stack.json +26 -0
- package/stacks/solidjs/skills/solidjs-patterns/SKILL.md +66 -0
- package/stacks/solidjs/stack.json +52 -0
- package/stacks/vanilla-extract/skills/vanilla-extract-patterns/SKILL.md +76 -0
- package/stacks/vanilla-extract/stack.json +40 -0
- package/templates/claude-toolkit.config.ts +34 -0
- package/templates/configs/biome.base.json +35 -0
- package/templates/configs/tsconfig.base.json +16 -0
- 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
|
+
```
|