@grimoire-cc/cli 0.4.1 → 0.5.0
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/dist/static/log-viewer.html +1 -0
- package/dist/static/static/log-viewer.html +1 -0
- package/package.json +1 -1
- package/packs/ts-pack/grimoire.json +33 -0
- package/packs/ts-pack/skills/grimoire:modern-typescript/SKILL.md +336 -0
- package/packs/ts-pack/skills/grimoire:modern-typescript/reference/modern-features.md +373 -0
- package/packs/ts-pack/skills/grimoire:modern-typescript/reference/patterns-and-idioms.md +477 -0
- package/packs/ts-pack/skills/grimoire:modern-typescript/reference/type-system.md +389 -0
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
# Type System Patterns
|
|
2
|
+
|
|
3
|
+
## Table of Contents
|
|
4
|
+
|
|
5
|
+
- [Generics Best Practices](#generics-best-practices)
|
|
6
|
+
- [Conditional Types](#conditional-types)
|
|
7
|
+
- [Mapped Types](#mapped-types)
|
|
8
|
+
- [Template Literal Types](#template-literal-types)
|
|
9
|
+
- [Type Guards and Narrowing](#type-guards-and-narrowing)
|
|
10
|
+
- [Utility Type Recipes](#utility-type-recipes)
|
|
11
|
+
- [Recursive Types](#recursive-types)
|
|
12
|
+
|
|
13
|
+
## Generics Best Practices
|
|
14
|
+
|
|
15
|
+
### Constrain Early, Infer Late
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
// Bad — too loose
|
|
19
|
+
function getProperty<T>(obj: T, key: string): unknown {
|
|
20
|
+
return (obj as Record<string, unknown>)[key];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Good — constrained and type-safe
|
|
24
|
+
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
|
|
25
|
+
return obj[key];
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### Name Generic Parameters Meaningfully
|
|
30
|
+
|
|
31
|
+
```typescript
|
|
32
|
+
// Bad — single-letter overload
|
|
33
|
+
function transform<T, U, V>(input: T, fn: (x: U) => V): V { /* ... */ }
|
|
34
|
+
|
|
35
|
+
// Good — descriptive names for complex generics
|
|
36
|
+
function transform<TInput, TIntermediate, TOutput>(
|
|
37
|
+
input: TInput,
|
|
38
|
+
fn: (x: TIntermediate) => TOutput,
|
|
39
|
+
): TOutput { /* ... */ }
|
|
40
|
+
|
|
41
|
+
// Single-letter is fine for simple, well-understood generics
|
|
42
|
+
function identity<T>(value: T): T { return value; }
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Use `const` Type Parameters for Literal Inference
|
|
46
|
+
|
|
47
|
+
```typescript
|
|
48
|
+
function createConfig<const T extends Record<string, unknown>>(config: T): T {
|
|
49
|
+
return config;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Infers { readonly host: "localhost"; readonly port: 3000 }
|
|
53
|
+
const cfg = createConfig({ host: "localhost", port: 3000 });
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Default Type Parameters
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
interface EventEmitter<TEvents extends Record<string, unknown[]> = Record<string, unknown[]>> {
|
|
60
|
+
on<K extends keyof TEvents>(event: K, handler: (...args: TEvents[K]) => void): void;
|
|
61
|
+
emit<K extends keyof TEvents>(event: K, ...args: TEvents[K]): void;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Works without explicit type argument
|
|
65
|
+
const emitter: EventEmitter = createEmitter();
|
|
66
|
+
|
|
67
|
+
// Or with specific events
|
|
68
|
+
interface AppEvents {
|
|
69
|
+
login: [userId: string];
|
|
70
|
+
error: [code: number, message: string];
|
|
71
|
+
}
|
|
72
|
+
const app: EventEmitter<AppEvents> = createEmitter();
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Avoid Over-Generification
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
// Bad — generic adds no value here
|
|
79
|
+
function add<T extends number>(a: T, b: T): number {
|
|
80
|
+
return a + b;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Good — simple parameter types when generics don't help
|
|
84
|
+
function add(a: number, b: number): number {
|
|
85
|
+
return a + b;
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Conditional Types
|
|
90
|
+
|
|
91
|
+
### Basic Pattern
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
type IsString<T> = T extends string ? true : false;
|
|
95
|
+
|
|
96
|
+
type A = IsString<"hello">; // true
|
|
97
|
+
type B = IsString<42>; // false
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Extracting from Unions
|
|
101
|
+
|
|
102
|
+
```typescript
|
|
103
|
+
// Extract only string members from a union
|
|
104
|
+
type StringMembers<T> = T extends string ? T : never;
|
|
105
|
+
|
|
106
|
+
type Mixed = "hello" | 42 | "world" | true;
|
|
107
|
+
type OnlyStrings = StringMembers<Mixed>; // "hello" | "world"
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### `infer` for Type Extraction
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
// Extract return type of a function
|
|
114
|
+
type ReturnOf<T> = T extends (...args: unknown[]) => infer R ? R : never;
|
|
115
|
+
|
|
116
|
+
// Extract element type from array
|
|
117
|
+
type ElementOf<T> = T extends readonly (infer E)[] ? E : never;
|
|
118
|
+
|
|
119
|
+
// Extract promise value
|
|
120
|
+
type Awaited<T> = T extends Promise<infer V> ? Awaited<V> : T;
|
|
121
|
+
|
|
122
|
+
// Extract specific tuple positions
|
|
123
|
+
type First<T> = T extends [infer F, ...unknown[]] ? F : never;
|
|
124
|
+
type Last<T> = T extends [...unknown[], infer L] ? L : never;
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Distributive vs. Non-Distributive
|
|
128
|
+
|
|
129
|
+
```typescript
|
|
130
|
+
// Distributive — applies to each union member separately
|
|
131
|
+
type ToArray<T> = T extends unknown ? T[] : never;
|
|
132
|
+
type Result = ToArray<string | number>; // string[] | number[]
|
|
133
|
+
|
|
134
|
+
// Non-distributive — wrapping in tuple prevents distribution
|
|
135
|
+
type ToArrayND<T> = [T] extends [unknown] ? T[] : never;
|
|
136
|
+
type Result2 = ToArrayND<string | number>; // (string | number)[]
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## Mapped Types
|
|
140
|
+
|
|
141
|
+
### Basic Transformation
|
|
142
|
+
|
|
143
|
+
```typescript
|
|
144
|
+
// Make all properties optional
|
|
145
|
+
type Optional<T> = { [K in keyof T]?: T[K] };
|
|
146
|
+
|
|
147
|
+
// Make all properties required
|
|
148
|
+
type Required<T> = { [K in keyof T]-?: T[K] };
|
|
149
|
+
|
|
150
|
+
// Make all properties readonly
|
|
151
|
+
type Immutable<T> = { readonly [K in keyof T]: T[K] };
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Key Remapping with `as`
|
|
155
|
+
|
|
156
|
+
```typescript
|
|
157
|
+
// Prefix all keys
|
|
158
|
+
type Prefixed<T, P extends string> = {
|
|
159
|
+
[K in keyof T as `${P}${string & K}`]: T[K];
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
interface User { name: string; age: number }
|
|
163
|
+
type PrefixedUser = Prefixed<User, "user_">;
|
|
164
|
+
// { user_name: string; user_age: number }
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Filtering Properties
|
|
168
|
+
|
|
169
|
+
```typescript
|
|
170
|
+
// Keep only string-valued properties
|
|
171
|
+
type StringProps<T> = {
|
|
172
|
+
[K in keyof T as T[K] extends string ? K : never]: T[K];
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
interface Mixed { name: string; age: number; email: string }
|
|
176
|
+
type OnlyStrings = StringProps<Mixed>;
|
|
177
|
+
// { name: string; email: string }
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### Deep Readonly
|
|
181
|
+
|
|
182
|
+
```typescript
|
|
183
|
+
type DeepReadonly<T> = T extends Function
|
|
184
|
+
? T
|
|
185
|
+
: T extends object
|
|
186
|
+
? { readonly [K in keyof T]: DeepReadonly<T[K]> }
|
|
187
|
+
: T;
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
## Template Literal Types
|
|
191
|
+
|
|
192
|
+
### Event System
|
|
193
|
+
|
|
194
|
+
```typescript
|
|
195
|
+
type EventName = "click" | "hover" | "focus";
|
|
196
|
+
type HandlerName = `on${Capitalize<EventName>}`;
|
|
197
|
+
// "onClick" | "onHover" | "onFocus"
|
|
198
|
+
|
|
199
|
+
type EventHandlers = {
|
|
200
|
+
[K in EventName as `on${Capitalize<K>}`]: (event: Event) => void;
|
|
201
|
+
};
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### Route Parameters
|
|
205
|
+
|
|
206
|
+
```typescript
|
|
207
|
+
type ExtractParams<T extends string> =
|
|
208
|
+
T extends `${string}:${infer Param}/${infer Rest}`
|
|
209
|
+
? { [K in Param | keyof ExtractParams<Rest>]: string }
|
|
210
|
+
: T extends `${string}:${infer Param}`
|
|
211
|
+
? { [K in Param]: string }
|
|
212
|
+
: Record<string, never>;
|
|
213
|
+
|
|
214
|
+
type Params = ExtractParams<"/users/:userId/posts/:postId">;
|
|
215
|
+
// { userId: string; postId: string }
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### String Manipulation Types
|
|
219
|
+
|
|
220
|
+
```typescript
|
|
221
|
+
// Built-in intrinsic types
|
|
222
|
+
type Upper = Uppercase<"hello">; // "HELLO"
|
|
223
|
+
type Lower = Lowercase<"HELLO">; // "hello"
|
|
224
|
+
type Cap = Capitalize<"hello">; // "Hello"
|
|
225
|
+
type Uncap = Uncapitalize<"Hello">; // "hello"
|
|
226
|
+
|
|
227
|
+
// Combine for conventions
|
|
228
|
+
type CamelToSnake<S extends string> =
|
|
229
|
+
S extends `${infer Head}${infer Tail}`
|
|
230
|
+
? Tail extends Uncapitalize<Tail>
|
|
231
|
+
? `${Lowercase<Head>}${CamelToSnake<Tail>}`
|
|
232
|
+
: `${Lowercase<Head>}_${CamelToSnake<Tail>}`
|
|
233
|
+
: S;
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
## Type Guards and Narrowing
|
|
237
|
+
|
|
238
|
+
### Custom Type Guards
|
|
239
|
+
|
|
240
|
+
```typescript
|
|
241
|
+
// Type predicate — explicitly declares narrowing
|
|
242
|
+
function isUser(value: unknown): value is User {
|
|
243
|
+
return (
|
|
244
|
+
typeof value === "object" &&
|
|
245
|
+
value !== null &&
|
|
246
|
+
"id" in value &&
|
|
247
|
+
"name" in value
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Assertion function — throws or narrows
|
|
252
|
+
function assertUser(value: unknown): asserts value is User {
|
|
253
|
+
if (!isUser(value)) {
|
|
254
|
+
throw new TypeError("Expected User");
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
### Inferred Type Predicates (TS 5.5+)
|
|
260
|
+
|
|
261
|
+
```typescript
|
|
262
|
+
// TypeScript now infers the type predicate automatically
|
|
263
|
+
const users = items.filter((item) => item.type === "user");
|
|
264
|
+
// Inferred as User[] — no explicit type guard needed
|
|
265
|
+
|
|
266
|
+
// Also works with nullish filtering
|
|
267
|
+
const nonNull = items.filter((item) => item != null);
|
|
268
|
+
// Inferred as NonNullable<T>[]
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
### Narrowing Patterns
|
|
272
|
+
|
|
273
|
+
```typescript
|
|
274
|
+
// `in` operator narrows
|
|
275
|
+
function handle(shape: Circle | Square) {
|
|
276
|
+
if ("radius" in shape) {
|
|
277
|
+
// shape is Circle
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// typeof narrows
|
|
282
|
+
function process(value: string | number) {
|
|
283
|
+
if (typeof value === "string") {
|
|
284
|
+
// value is string
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// instanceof narrows
|
|
289
|
+
function handleError(error: unknown) {
|
|
290
|
+
if (error instanceof TypeError) {
|
|
291
|
+
// error is TypeError
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Discriminant property narrows
|
|
296
|
+
function handleResult(result: Result<User>) {
|
|
297
|
+
if (result.ok) {
|
|
298
|
+
// result is { ok: true; value: User }
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
## Utility Type Recipes
|
|
304
|
+
|
|
305
|
+
### Pick and Omit Variants
|
|
306
|
+
|
|
307
|
+
```typescript
|
|
308
|
+
// Pick only required properties
|
|
309
|
+
type RequiredKeys<T> = {
|
|
310
|
+
[K in keyof T]-?: undefined extends T[K] ? never : K;
|
|
311
|
+
}[keyof T];
|
|
312
|
+
|
|
313
|
+
type RequiredPick<T> = Pick<T, RequiredKeys<T>>;
|
|
314
|
+
|
|
315
|
+
// Make specific properties optional
|
|
316
|
+
type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
|
|
317
|
+
|
|
318
|
+
// Make specific properties required
|
|
319
|
+
type RequiredBy<T, K extends keyof T> = Omit<T, K> & Required<Pick<T, K>>;
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
### Strict Omit
|
|
323
|
+
|
|
324
|
+
```typescript
|
|
325
|
+
// Built-in Omit doesn't check keys — this one does
|
|
326
|
+
type StrictOmit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
### Union to Intersection
|
|
330
|
+
|
|
331
|
+
```typescript
|
|
332
|
+
type UnionToIntersection<U> =
|
|
333
|
+
(U extends unknown ? (arg: U) => void : never) extends (arg: infer I) => void
|
|
334
|
+
? I
|
|
335
|
+
: never;
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
### Merge Types
|
|
339
|
+
|
|
340
|
+
```typescript
|
|
341
|
+
// Like intersection but shows resolved keys in IDE
|
|
342
|
+
type Merge<A, B> = {
|
|
343
|
+
[K in keyof A | keyof B]: K extends keyof B
|
|
344
|
+
? B[K]
|
|
345
|
+
: K extends keyof A
|
|
346
|
+
? A[K]
|
|
347
|
+
: never;
|
|
348
|
+
};
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
## Recursive Types
|
|
352
|
+
|
|
353
|
+
### Deep Partial
|
|
354
|
+
|
|
355
|
+
```typescript
|
|
356
|
+
type DeepPartial<T> = T extends object
|
|
357
|
+
? { [K in keyof T]?: DeepPartial<T[K]> }
|
|
358
|
+
: T;
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
### JSON Type
|
|
362
|
+
|
|
363
|
+
```typescript
|
|
364
|
+
type JsonValue =
|
|
365
|
+
| string
|
|
366
|
+
| number
|
|
367
|
+
| boolean
|
|
368
|
+
| null
|
|
369
|
+
| JsonValue[]
|
|
370
|
+
| { [key: string]: JsonValue };
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
### Nested Path Types
|
|
374
|
+
|
|
375
|
+
```typescript
|
|
376
|
+
type Path<T, K extends keyof T = keyof T> = K extends string
|
|
377
|
+
? T[K] extends object
|
|
378
|
+
? K | `${K}.${Path<T[K]>}`
|
|
379
|
+
: K
|
|
380
|
+
: never;
|
|
381
|
+
|
|
382
|
+
interface Config {
|
|
383
|
+
db: { host: string; port: number };
|
|
384
|
+
app: { name: string };
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
type ConfigPaths = Path<Config>;
|
|
388
|
+
// "db" | "db.host" | "db.port" | "app" | "app.name"
|
|
389
|
+
```
|