clearctx 3.0.0 → 3.1.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/README.md +1 -0
- package/bin/setup.js +33 -1
- package/package.json +3 -2
- package/skills/api-design/SKILL.md +796 -0
- package/skills/devops/SKILL.md +1043 -0
- package/skills/index.json +53 -0
- package/skills/nodejs-backend/SKILL.md +853 -0
- package/skills/postgresql/SKILL.md +315 -0
- package/skills/react-frontend/SKILL.md +683 -0
- package/skills/security/SKILL.md +1000 -0
- package/skills/testing-qa/SKILL.md +842 -0
- package/skills/typescript/SKILL.md +932 -0
- package/src/mcp-server.js +126 -1
- package/src/prompts.js +47 -2
- package/src/skill-registry.js +182 -0
- package/src/stream-session.js +22 -2
- package/STRATEGY.md +0 -485
|
@@ -0,0 +1,932 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: typescript
|
|
3
|
+
description: Production-grade TypeScript patterns for type-first development, generics, strict mode, type narrowing, and module architecture
|
|
4
|
+
domain: language
|
|
5
|
+
keywords: [typescript, types, generics, interfaces, strict-mode, type-guards, utility-types, tsconfig]
|
|
6
|
+
version: 1.0.0
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# TypeScript Expertise Skill
|
|
10
|
+
|
|
11
|
+
## Worker Context
|
|
12
|
+
|
|
13
|
+
You are a TypeScript expert worker. Apply these patterns to ALL code you write.
|
|
14
|
+
|
|
15
|
+
### Type-First Development
|
|
16
|
+
|
|
17
|
+
**CRITICAL:** Define types/interfaces BEFORE implementation. Types ARE your documentation.
|
|
18
|
+
|
|
19
|
+
```typescript
|
|
20
|
+
// GOOD: Type-first approach
|
|
21
|
+
interface User {
|
|
22
|
+
id: string;
|
|
23
|
+
email: string;
|
|
24
|
+
role: 'admin' | 'user';
|
|
25
|
+
createdAt: Date;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface CreateUserRequest {
|
|
29
|
+
email: string;
|
|
30
|
+
password: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function createUser(req: CreateUserRequest): User {
|
|
34
|
+
// Implementation follows types
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// BAD: Implementation-first, types retrofitted
|
|
38
|
+
function createUser(email, password) {
|
|
39
|
+
return { id: generateId(), email, role: 'user', createdAt: new Date() };
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
**IMPORTANT:** Export types from module index. Shared types go in `types/` directory.
|
|
44
|
+
|
|
45
|
+
### Interface vs Type Decision Tree
|
|
46
|
+
|
|
47
|
+
| Use Case | Use This | Why |
|
|
48
|
+
|----------|----------|-----|
|
|
49
|
+
| Object shapes (may extend later) | `interface` | Declaration merging, cleaner extends |
|
|
50
|
+
| Unions, intersections, primitives | `type` | Only types support `A \| B`, `A & B`, mapped types |
|
|
51
|
+
| Function signatures | `type` | Cleaner: `type Handler = (req: Request) => Response` |
|
|
52
|
+
| Class contracts | `interface` | `implements` keyword, natural fit |
|
|
53
|
+
| When either works | `interface` | Slightly better error messages, extendable |
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
// GOOD: Correct tool selection
|
|
57
|
+
interface User {
|
|
58
|
+
id: string;
|
|
59
|
+
name: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
interface AdminUser extends User {
|
|
63
|
+
permissions: string[];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
type Result<T> = { success: true; data: T } | { success: false; error: string };
|
|
67
|
+
type Handler = (req: Request) => Promise<Response>;
|
|
68
|
+
type Nullable<T> = T | null;
|
|
69
|
+
|
|
70
|
+
// BAD: Wrong tool for the job
|
|
71
|
+
type User = { // Should be interface (object shape)
|
|
72
|
+
id: string;
|
|
73
|
+
name: string;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
interface Result<T> { // Cannot express union with interface
|
|
77
|
+
success: boolean;
|
|
78
|
+
data?: T;
|
|
79
|
+
error?: string;
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Generics
|
|
84
|
+
|
|
85
|
+
**Constrain generics with `extends`** — NEVER add generics that don't constrain anything.
|
|
86
|
+
|
|
87
|
+
```typescript
|
|
88
|
+
// GOOD: Meaningful constraints
|
|
89
|
+
function findById<T extends { id: string }>(items: T[], id: string): T | undefined {
|
|
90
|
+
return items.find(item => item.id === id);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
type ApiResponse<TData> = {
|
|
94
|
+
data: TData;
|
|
95
|
+
timestamp: number;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
function processItems<T extends { status: string }>(
|
|
99
|
+
items: T[],
|
|
100
|
+
filter: (item: T) => boolean
|
|
101
|
+
): T[] {
|
|
102
|
+
return items.filter(filter);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// BAD: Unconstrained generics (just use concrete types)
|
|
106
|
+
function log<T>(value: T): void { // T adds no value here
|
|
107
|
+
console.log(value);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// GOOD: No generic needed
|
|
111
|
+
function log(value: unknown): void {
|
|
112
|
+
console.log(value);
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
**Use descriptive names for complex generics:** `TResponse`, `TInput`, `TEntity`, not just `T`, `U`, `V`.
|
|
117
|
+
|
|
118
|
+
### Strict Mode Configuration
|
|
119
|
+
|
|
120
|
+
**CRITICAL:** Always enable in `tsconfig.json`:
|
|
121
|
+
|
|
122
|
+
```json
|
|
123
|
+
{
|
|
124
|
+
"compilerOptions": {
|
|
125
|
+
"strict": true,
|
|
126
|
+
"noUncheckedIndexedAccess": true,
|
|
127
|
+
"exactOptionalPropertyTypes": true,
|
|
128
|
+
"noImplicitOverride": true,
|
|
129
|
+
"noImplicitReturns": true,
|
|
130
|
+
"noFallthroughCasesInSwitch": true
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
**NEVER use `// @ts-ignore`** unless fixing third-party type bugs — add comment explaining why.
|
|
136
|
+
|
|
137
|
+
```typescript
|
|
138
|
+
// BAD: Silencing errors
|
|
139
|
+
// @ts-ignore
|
|
140
|
+
const result = dangerousOperation();
|
|
141
|
+
|
|
142
|
+
// GOOD: Fix the type issue
|
|
143
|
+
const result = dangerousOperation() as Result;
|
|
144
|
+
|
|
145
|
+
// ACCEPTABLE: Third-party bug with explanation
|
|
146
|
+
// @ts-ignore — bug in @types/legacy-lib v3.2.1, fixed in v3.3.0
|
|
147
|
+
// See: https://github.com/DefinitelyTyped/DefinitelyTyped/issues/12345
|
|
148
|
+
import { brokenFunction } from 'legacy-lib';
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### Null Safety
|
|
152
|
+
|
|
153
|
+
**CRITICAL:** Strict null checks ON. Use `??` for defaults, `?.` for optional access.
|
|
154
|
+
|
|
155
|
+
```typescript
|
|
156
|
+
// GOOD: Null-safe patterns
|
|
157
|
+
const username = user?.profile?.name ?? 'Anonymous';
|
|
158
|
+
const config = loadConfig() ?? getDefaults();
|
|
159
|
+
|
|
160
|
+
type Result<T> =
|
|
161
|
+
| { success: true; data: T }
|
|
162
|
+
| { success: false; error: string };
|
|
163
|
+
|
|
164
|
+
function processResult<T>(result: Result<T>): void {
|
|
165
|
+
if (result.success) {
|
|
166
|
+
console.log(result.data); // TypeScript knows data exists
|
|
167
|
+
} else {
|
|
168
|
+
console.error(result.error); // TypeScript knows error exists
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// BAD: Non-null assertions without guards
|
|
173
|
+
const name = user!.profile!.name; // Crashes if user/profile is null
|
|
174
|
+
|
|
175
|
+
// GOOD: Type guard first
|
|
176
|
+
if (user?.profile) {
|
|
177
|
+
const name = user.profile.name; // Safe
|
|
178
|
+
}
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
**NEVER use non-null assertion `!`** without explicit type guard first.
|
|
182
|
+
|
|
183
|
+
### Enum Patterns
|
|
184
|
+
|
|
185
|
+
**Prefer `as const` objects over enums** for tree-shaking:
|
|
186
|
+
|
|
187
|
+
```typescript
|
|
188
|
+
// GOOD: const object (tree-shakable, no runtime overhead)
|
|
189
|
+
const STATUS = {
|
|
190
|
+
PENDING: 'pending',
|
|
191
|
+
ACTIVE: 'active',
|
|
192
|
+
COMPLETED: 'completed',
|
|
193
|
+
} as const;
|
|
194
|
+
|
|
195
|
+
type Status = typeof STATUS[keyof typeof STATUS]; // 'pending' | 'active' | 'completed'
|
|
196
|
+
|
|
197
|
+
function updateStatus(status: Status): void {
|
|
198
|
+
if (status === STATUS.PENDING) { /* ... */ }
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// ACCEPTABLE: String enum when you need runtime iteration
|
|
202
|
+
enum Color {
|
|
203
|
+
Red = 'red',
|
|
204
|
+
Green = 'green',
|
|
205
|
+
Blue = 'blue',
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// BAD: Numeric enum (ambiguous reverse mapping)
|
|
209
|
+
enum Status {
|
|
210
|
+
Pending, // 0
|
|
211
|
+
Active, // 1
|
|
212
|
+
Completed // 2
|
|
213
|
+
}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### Type Narrowing
|
|
217
|
+
|
|
218
|
+
**Use discriminated unions** (tagged unions with `kind`/`type` field):
|
|
219
|
+
|
|
220
|
+
```typescript
|
|
221
|
+
// GOOD: Discriminated union
|
|
222
|
+
type ApiState<T> =
|
|
223
|
+
| { status: 'idle' }
|
|
224
|
+
| { status: 'loading' }
|
|
225
|
+
| { status: 'success'; data: T }
|
|
226
|
+
| { status: 'error'; error: string };
|
|
227
|
+
|
|
228
|
+
function render<T>(state: ApiState<T>): string {
|
|
229
|
+
switch (state.status) {
|
|
230
|
+
case 'idle':
|
|
231
|
+
return 'Not started';
|
|
232
|
+
case 'loading':
|
|
233
|
+
return 'Loading...';
|
|
234
|
+
case 'success':
|
|
235
|
+
return `Data: ${state.data}`; // TypeScript knows data exists
|
|
236
|
+
case 'error':
|
|
237
|
+
return `Error: ${state.error}`; // TypeScript knows error exists
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Custom type guard
|
|
242
|
+
function isUser(value: unknown): value is User {
|
|
243
|
+
return (
|
|
244
|
+
typeof value === 'object' &&
|
|
245
|
+
value !== null &&
|
|
246
|
+
'id' in value &&
|
|
247
|
+
'email' in value
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Use 'in' operator for narrowing
|
|
252
|
+
function processValue(value: string | { message: string }): string {
|
|
253
|
+
if (typeof value === 'string') {
|
|
254
|
+
return value;
|
|
255
|
+
}
|
|
256
|
+
return value.message;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Use 'satisfies' for type checking without widening
|
|
260
|
+
const config = {
|
|
261
|
+
host: 'localhost',
|
|
262
|
+
port: 3000,
|
|
263
|
+
} satisfies Record<string, string | number>;
|
|
264
|
+
|
|
265
|
+
config.port; // Type is still number, not string | number
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
**NEVER use type assertions instead of narrowing:**
|
|
269
|
+
|
|
270
|
+
```typescript
|
|
271
|
+
// BAD: Type assertion
|
|
272
|
+
function getUser(data: unknown): User {
|
|
273
|
+
return data as User; // No runtime validation
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// GOOD: Type guard
|
|
277
|
+
function getUser(data: unknown): User {
|
|
278
|
+
if (!isUser(data)) {
|
|
279
|
+
throw new Error('Invalid user data');
|
|
280
|
+
}
|
|
281
|
+
return data;
|
|
282
|
+
}
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
### Utility Types Reference
|
|
286
|
+
|
|
287
|
+
| Type | Use When | Example |
|
|
288
|
+
|------|----------|---------|
|
|
289
|
+
| `Partial<T>` | Optional update payload | `Partial<User>` for PATCH body |
|
|
290
|
+
| `Required<T>` | Ensure all fields present | Config after defaults applied |
|
|
291
|
+
| `Pick<T, K>` | Subset of fields | `Pick<User, 'id' \| 'name'>` |
|
|
292
|
+
| `Omit<T, K>` | Exclude fields | `Omit<User, 'password'>` for API response |
|
|
293
|
+
| `Record<K, V>` | Typed dictionaries | `Record<string, Handler>` |
|
|
294
|
+
| `ReturnType<F>` | Infer function return | `ReturnType<typeof getUser>` |
|
|
295
|
+
| `Parameters<F>` | Infer function args | `Parameters<typeof handler>[0]` |
|
|
296
|
+
| `Awaited<T>` | Unwrap Promise | `Awaited<ReturnType<typeof fetchUser>>` |
|
|
297
|
+
| `NonNullable<T>` | Exclude null/undefined | `NonNullable<string \| null>` |
|
|
298
|
+
|
|
299
|
+
```typescript
|
|
300
|
+
// Common utility type patterns
|
|
301
|
+
type UserUpdatePayload = Partial<Omit<User, 'id' | 'createdAt'>>;
|
|
302
|
+
|
|
303
|
+
type ApiHandler = (req: Request) => Promise<Response>;
|
|
304
|
+
type HandlerParams = Parameters<ApiHandler>[0];
|
|
305
|
+
type HandlerReturn = Awaited<ReturnType<ApiHandler>>;
|
|
306
|
+
|
|
307
|
+
type UserPublic = Omit<User, 'password' | 'passwordHash'>;
|
|
308
|
+
type UserCache = Pick<User, 'id' | 'email' | 'role'>;
|
|
309
|
+
|
|
310
|
+
type StatusMap = Record<Status, { label: string; color: string }>;
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
### Module Patterns
|
|
314
|
+
|
|
315
|
+
**Barrel exports** — `index.ts` per feature:
|
|
316
|
+
|
|
317
|
+
```typescript
|
|
318
|
+
// src/users/index.ts
|
|
319
|
+
export { createUser, updateUser, deleteUser } from './user.service';
|
|
320
|
+
export { UserController } from './user.controller';
|
|
321
|
+
export type { User, CreateUserRequest, UpdateUserRequest } from './user.types';
|
|
322
|
+
|
|
323
|
+
// Import from barrel
|
|
324
|
+
import { createUser, type User } from '@/users';
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
**Path aliases** — configure in `tsconfig.json`:
|
|
328
|
+
|
|
329
|
+
```json
|
|
330
|
+
{
|
|
331
|
+
"compilerOptions": {
|
|
332
|
+
"baseUrl": ".",
|
|
333
|
+
"paths": {
|
|
334
|
+
"@/*": ["src/*"],
|
|
335
|
+
"@/types": ["src/types"]
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
**Declaration files** — for untyped third-party libs:
|
|
342
|
+
|
|
343
|
+
```typescript
|
|
344
|
+
// src/types/legacy-lib.d.ts
|
|
345
|
+
declare module 'legacy-lib' {
|
|
346
|
+
export function doSomething(input: string): Promise<Result>;
|
|
347
|
+
export interface Result {
|
|
348
|
+
success: boolean;
|
|
349
|
+
data: unknown;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
**NEVER use `require()`** — always use `import`:
|
|
355
|
+
|
|
356
|
+
```typescript
|
|
357
|
+
// BAD
|
|
358
|
+
const express = require('express');
|
|
359
|
+
|
|
360
|
+
// GOOD
|
|
361
|
+
import express from 'express';
|
|
362
|
+
import type { Request, Response } from 'express';
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
### Error Handling
|
|
366
|
+
|
|
367
|
+
**Typed error classes:**
|
|
368
|
+
|
|
369
|
+
```typescript
|
|
370
|
+
// GOOD: Type-safe error handling
|
|
371
|
+
class AppError extends Error {
|
|
372
|
+
constructor(
|
|
373
|
+
public code: string,
|
|
374
|
+
message: string,
|
|
375
|
+
public statusCode: number = 500
|
|
376
|
+
) {
|
|
377
|
+
super(message);
|
|
378
|
+
this.name = 'AppError';
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
class ValidationError extends AppError {
|
|
383
|
+
constructor(message: string, public fields: string[]) {
|
|
384
|
+
super('VALIDATION_ERROR', message, 400);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Result pattern for expected failures
|
|
389
|
+
type Result<T, E = Error> =
|
|
390
|
+
| { ok: true; value: T }
|
|
391
|
+
| { ok: false; error: E };
|
|
392
|
+
|
|
393
|
+
function parseUser(data: unknown): Result<User, ValidationError> {
|
|
394
|
+
if (!isUser(data)) {
|
|
395
|
+
return { ok: false, error: new ValidationError('Invalid user', []) };
|
|
396
|
+
}
|
|
397
|
+
return { ok: true, value: data };
|
|
398
|
+
}
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
**Exhaustive switch checks with `never`:**
|
|
402
|
+
|
|
403
|
+
```typescript
|
|
404
|
+
type Action =
|
|
405
|
+
| { type: 'create'; payload: User }
|
|
406
|
+
| { type: 'update'; payload: Partial<User> }
|
|
407
|
+
| { type: 'delete'; payload: string };
|
|
408
|
+
|
|
409
|
+
function reducer(action: Action): void {
|
|
410
|
+
switch (action.type) {
|
|
411
|
+
case 'create':
|
|
412
|
+
return handleCreate(action.payload);
|
|
413
|
+
case 'update':
|
|
414
|
+
return handleUpdate(action.payload);
|
|
415
|
+
case 'delete':
|
|
416
|
+
return handleDelete(action.payload);
|
|
417
|
+
default:
|
|
418
|
+
// If we add a new action type and forget to handle it, TypeScript errors here
|
|
419
|
+
const _exhaustive: never = action;
|
|
420
|
+
throw new Error(`Unhandled action: ${_exhaustive}`);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
### tsconfig.json Best Practices
|
|
426
|
+
|
|
427
|
+
```json
|
|
428
|
+
{
|
|
429
|
+
"compilerOptions": {
|
|
430
|
+
// Modern JavaScript
|
|
431
|
+
"target": "ES2022",
|
|
432
|
+
"lib": ["ES2022"],
|
|
433
|
+
|
|
434
|
+
// Module system
|
|
435
|
+
"module": "NodeNext", // For Node.js
|
|
436
|
+
"moduleResolution": "NodeNext", // For Node.js
|
|
437
|
+
// OR for bundlers:
|
|
438
|
+
// "module": "ESNext",
|
|
439
|
+
// "moduleResolution": "bundler",
|
|
440
|
+
|
|
441
|
+
// Emit
|
|
442
|
+
"outDir": "./dist",
|
|
443
|
+
"declaration": true, // For libraries
|
|
444
|
+
"declarationMap": true,
|
|
445
|
+
"sourceMap": true,
|
|
446
|
+
|
|
447
|
+
// Strict checks (CRITICAL)
|
|
448
|
+
"strict": true,
|
|
449
|
+
"noUncheckedIndexedAccess": true,
|
|
450
|
+
"exactOptionalPropertyTypes": true,
|
|
451
|
+
"noImplicitOverride": true,
|
|
452
|
+
"noImplicitReturns": true,
|
|
453
|
+
"noFallthroughCasesInSwitch": true,
|
|
454
|
+
|
|
455
|
+
// Module resolution
|
|
456
|
+
"esModuleInterop": true,
|
|
457
|
+
"allowSyntheticDefaultImports": true,
|
|
458
|
+
"resolveJsonModule": true,
|
|
459
|
+
|
|
460
|
+
// Monorepo support
|
|
461
|
+
"composite": true, // For project references
|
|
462
|
+
|
|
463
|
+
// Path aliases
|
|
464
|
+
"baseUrl": ".",
|
|
465
|
+
"paths": {
|
|
466
|
+
"@/*": ["src/*"]
|
|
467
|
+
},
|
|
468
|
+
|
|
469
|
+
// Performance
|
|
470
|
+
"skipLibCheck": true,
|
|
471
|
+
"incremental": true
|
|
472
|
+
},
|
|
473
|
+
"include": ["src/**/*"],
|
|
474
|
+
"exclude": ["node_modules", "dist", "**/*.test.ts"]
|
|
475
|
+
}
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
## Conventions
|
|
479
|
+
|
|
480
|
+
1. **Naming:**
|
|
481
|
+
- Interfaces/Types: PascalCase — `User`, `CreateUserRequest`, `ApiResponse<T>`
|
|
482
|
+
- Type parameters: Single letter for simple (`T`, `K`, `V`) or descriptive prefix for complex (`TData`, `TResponse`, `TEntity`)
|
|
483
|
+
- Enums/const objects: UPPER_CASE keys — `STATUS.PENDING`, `Color.Red`
|
|
484
|
+
- Files: kebab-case — `user.service.ts`, `api-client.ts`
|
|
485
|
+
|
|
486
|
+
2. **Exports:**
|
|
487
|
+
- Always export types using `export type` for type-only exports
|
|
488
|
+
- Use barrel exports (`index.ts`) per feature module
|
|
489
|
+
- Shared types in `src/types/` or `@/types`
|
|
490
|
+
|
|
491
|
+
3. **Imports:**
|
|
492
|
+
- Group imports: (1) external packages, (2) internal absolute imports, (3) relative imports
|
|
493
|
+
- Type imports: `import type { User } from './types'` when only used in type position
|
|
494
|
+
|
|
495
|
+
4. **File structure:**
|
|
496
|
+
```
|
|
497
|
+
src/
|
|
498
|
+
types/ # Shared types
|
|
499
|
+
index.ts
|
|
500
|
+
user.types.ts
|
|
501
|
+
api.types.ts
|
|
502
|
+
users/
|
|
503
|
+
index.ts # Barrel export
|
|
504
|
+
user.service.ts
|
|
505
|
+
user.types.ts # Feature-specific types
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
## Common Patterns
|
|
509
|
+
|
|
510
|
+
### 1. API Response Types
|
|
511
|
+
|
|
512
|
+
```typescript
|
|
513
|
+
// Generic API response wrapper
|
|
514
|
+
type ApiResponse<T> = {
|
|
515
|
+
data: T;
|
|
516
|
+
meta: {
|
|
517
|
+
timestamp: number;
|
|
518
|
+
requestId: string;
|
|
519
|
+
};
|
|
520
|
+
};
|
|
521
|
+
|
|
522
|
+
type ApiError = {
|
|
523
|
+
error: {
|
|
524
|
+
code: string;
|
|
525
|
+
message: string;
|
|
526
|
+
details?: Record<string, unknown>;
|
|
527
|
+
};
|
|
528
|
+
};
|
|
529
|
+
|
|
530
|
+
type ApiResult<T> = ApiResponse<T> | ApiError;
|
|
531
|
+
|
|
532
|
+
// Type guard
|
|
533
|
+
function isApiError(response: ApiResult<unknown>): response is ApiError {
|
|
534
|
+
return 'error' in response;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// Usage
|
|
538
|
+
async function fetchUser(id: string): Promise<User> {
|
|
539
|
+
const response = await fetch(`/api/users/${id}`);
|
|
540
|
+
const json: ApiResult<User> = await response.json();
|
|
541
|
+
|
|
542
|
+
if (isApiError(json)) {
|
|
543
|
+
throw new AppError(json.error.code, json.error.message);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
return json.data;
|
|
547
|
+
}
|
|
548
|
+
```
|
|
549
|
+
|
|
550
|
+
### 2. Builder Pattern with Fluent Interface
|
|
551
|
+
|
|
552
|
+
```typescript
|
|
553
|
+
class QueryBuilder<T> {
|
|
554
|
+
private filters: Array<(item: T) => boolean> = [];
|
|
555
|
+
private sortFn?: (a: T, b: T) => number;
|
|
556
|
+
private limitValue?: number;
|
|
557
|
+
|
|
558
|
+
where(predicate: (item: T) => boolean): this {
|
|
559
|
+
this.filters.push(predicate);
|
|
560
|
+
return this;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
sortBy(compareFn: (a: T, b: T) => number): this {
|
|
564
|
+
this.sortFn = compareFn;
|
|
565
|
+
return this;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
limit(n: number): this {
|
|
569
|
+
this.limitValue = n;
|
|
570
|
+
return this;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
execute(items: T[]): T[] {
|
|
574
|
+
let result = items.filter(item =>
|
|
575
|
+
this.filters.every(filter => filter(item))
|
|
576
|
+
);
|
|
577
|
+
|
|
578
|
+
if (this.sortFn) {
|
|
579
|
+
result = result.sort(this.sortFn);
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
if (this.limitValue !== undefined) {
|
|
583
|
+
result = result.slice(0, this.limitValue);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
return result;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// Usage
|
|
591
|
+
const activeUsers = new QueryBuilder<User>()
|
|
592
|
+
.where(u => u.role === 'admin')
|
|
593
|
+
.where(u => u.isActive)
|
|
594
|
+
.sortBy((a, b) => a.name.localeCompare(b.name))
|
|
595
|
+
.limit(10)
|
|
596
|
+
.execute(allUsers);
|
|
597
|
+
```
|
|
598
|
+
|
|
599
|
+
### 3. Discriminated Union State Machine
|
|
600
|
+
|
|
601
|
+
```typescript
|
|
602
|
+
type ConnectionState =
|
|
603
|
+
| { status: 'disconnected' }
|
|
604
|
+
| { status: 'connecting'; startedAt: number }
|
|
605
|
+
| { status: 'connected'; socket: WebSocket; connectedAt: number }
|
|
606
|
+
| { status: 'error'; error: Error; lastAttempt: number };
|
|
607
|
+
|
|
608
|
+
class Connection {
|
|
609
|
+
private state: ConnectionState = { status: 'disconnected' };
|
|
610
|
+
|
|
611
|
+
connect(): void {
|
|
612
|
+
if (this.state.status === 'connected') {
|
|
613
|
+
return; // Already connected
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
this.state = { status: 'connecting', startedAt: Date.now() };
|
|
617
|
+
// ... connection logic
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
handleConnected(socket: WebSocket): void {
|
|
621
|
+
if (this.state.status !== 'connecting') {
|
|
622
|
+
throw new Error('Cannot transition to connected from ' + this.state.status);
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
this.state = { status: 'connected', socket, connectedAt: Date.now() };
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
getStatus(): string {
|
|
629
|
+
switch (this.state.status) {
|
|
630
|
+
case 'disconnected':
|
|
631
|
+
return 'Not connected';
|
|
632
|
+
case 'connecting':
|
|
633
|
+
return `Connecting... (${Date.now() - this.state.startedAt}ms)`;
|
|
634
|
+
case 'connected':
|
|
635
|
+
return `Connected for ${Date.now() - this.state.connectedAt}ms`;
|
|
636
|
+
case 'error':
|
|
637
|
+
return `Error: ${this.state.error.message}`;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
```
|
|
642
|
+
|
|
643
|
+
### 4. Mapped Types for Transformations
|
|
644
|
+
|
|
645
|
+
```typescript
|
|
646
|
+
// Make all properties of T into functions that return T[K]
|
|
647
|
+
type Getters<T> = {
|
|
648
|
+
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
|
|
649
|
+
};
|
|
650
|
+
|
|
651
|
+
// Make all properties of T into functions that accept T[K]
|
|
652
|
+
type Setters<T> = {
|
|
653
|
+
[K in keyof T as `set${Capitalize<string & K>}`]: (value: T[K]) => void;
|
|
654
|
+
};
|
|
655
|
+
|
|
656
|
+
type User = {
|
|
657
|
+
name: string;
|
|
658
|
+
age: number;
|
|
659
|
+
};
|
|
660
|
+
|
|
661
|
+
type UserGetters = Getters<User>;
|
|
662
|
+
// { getName: () => string; getAge: () => number; }
|
|
663
|
+
|
|
664
|
+
type UserSetters = Setters<User>;
|
|
665
|
+
// { setName: (value: string) => void; setAge: (value: number) => void; }
|
|
666
|
+
|
|
667
|
+
// Practical example: Create immutable update helpers
|
|
668
|
+
type UpdateFunctions<T> = {
|
|
669
|
+
[K in keyof T as `update${Capitalize<string & K>}`]: (value: T[K]) => T;
|
|
670
|
+
};
|
|
671
|
+
|
|
672
|
+
function createUpdateHelpers<T>(initial: T): UpdateFunctions<T> {
|
|
673
|
+
const helpers = {} as UpdateFunctions<T>;
|
|
674
|
+
|
|
675
|
+
for (const key in initial) {
|
|
676
|
+
const capitalizedKey = key.charAt(0).toUpperCase() + key.slice(1);
|
|
677
|
+
const helperName = `update${capitalizedKey}` as keyof UpdateFunctions<T>;
|
|
678
|
+
|
|
679
|
+
helpers[helperName] = ((value: T[typeof key]) => ({
|
|
680
|
+
...initial,
|
|
681
|
+
[key]: value,
|
|
682
|
+
})) as UpdateFunctions<T>[typeof helperName];
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
return helpers;
|
|
686
|
+
}
|
|
687
|
+
```
|
|
688
|
+
|
|
689
|
+
### 5. Type-Safe Event Emitter
|
|
690
|
+
|
|
691
|
+
```typescript
|
|
692
|
+
type EventMap = {
|
|
693
|
+
'user:created': { user: User };
|
|
694
|
+
'user:updated': { user: User; changes: Partial<User> };
|
|
695
|
+
'user:deleted': { userId: string };
|
|
696
|
+
};
|
|
697
|
+
|
|
698
|
+
class TypedEventEmitter<T extends Record<string, unknown>> {
|
|
699
|
+
private listeners = new Map<keyof T, Set<(data: unknown) => void>>();
|
|
700
|
+
|
|
701
|
+
on<K extends keyof T>(event: K, handler: (data: T[K]) => void): void {
|
|
702
|
+
if (!this.listeners.has(event)) {
|
|
703
|
+
this.listeners.set(event, new Set());
|
|
704
|
+
}
|
|
705
|
+
this.listeners.get(event)!.add(handler as (data: unknown) => void);
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
emit<K extends keyof T>(event: K, data: T[K]): void {
|
|
709
|
+
const handlers = this.listeners.get(event);
|
|
710
|
+
if (handlers) {
|
|
711
|
+
handlers.forEach(handler => handler(data));
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
off<K extends keyof T>(event: K, handler: (data: T[K]) => void): void {
|
|
716
|
+
const handlers = this.listeners.get(event);
|
|
717
|
+
if (handlers) {
|
|
718
|
+
handlers.delete(handler as (data: unknown) => void);
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
// Usage (fully type-safe)
|
|
724
|
+
const emitter = new TypedEventEmitter<EventMap>();
|
|
725
|
+
|
|
726
|
+
emitter.on('user:created', ({ user }) => {
|
|
727
|
+
console.log(`User created: ${user.name}`);
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
emitter.on('user:updated', ({ user, changes }) => {
|
|
731
|
+
console.log(`User ${user.id} updated:`, changes);
|
|
732
|
+
});
|
|
733
|
+
|
|
734
|
+
emitter.emit('user:created', { user: newUser });
|
|
735
|
+
// emitter.emit('user:created', { wrong: 'data' }); // Type error!
|
|
736
|
+
```
|
|
737
|
+
|
|
738
|
+
## Anti-Patterns
|
|
739
|
+
|
|
740
|
+
### 1. ❌ Using `any` Instead of `unknown`
|
|
741
|
+
|
|
742
|
+
```typescript
|
|
743
|
+
// BAD: any disables all type checking
|
|
744
|
+
function processData(data: any): void {
|
|
745
|
+
data.something.that.might.not.exist(); // No error, crashes at runtime
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
// GOOD: unknown forces type validation
|
|
749
|
+
function processData(data: unknown): void {
|
|
750
|
+
if (typeof data === 'object' && data !== null && 'id' in data) {
|
|
751
|
+
console.log((data as { id: string }).id);
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
// ACCEPTABLE: any at system boundaries with immediate validation
|
|
756
|
+
async function fetchData(url: string): Promise<User> {
|
|
757
|
+
const response = await fetch(url);
|
|
758
|
+
const data: any = await response.json(); // External data = any
|
|
759
|
+
|
|
760
|
+
if (!isUser(data)) { // Immediately validate
|
|
761
|
+
throw new Error('Invalid user data');
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
return data; // Now type-safe
|
|
765
|
+
}
|
|
766
|
+
```
|
|
767
|
+
|
|
768
|
+
### 2. ❌ Type Assertions Instead of Narrowing
|
|
769
|
+
|
|
770
|
+
```typescript
|
|
771
|
+
// BAD: Type assertion without validation
|
|
772
|
+
function getUser(data: unknown): User {
|
|
773
|
+
return data as User; // No runtime safety
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
const config = JSON.parse(configString) as Config; // Crashes if wrong shape
|
|
777
|
+
|
|
778
|
+
// GOOD: Type guard with validation
|
|
779
|
+
function getUser(data: unknown): User {
|
|
780
|
+
if (!isUser(data)) {
|
|
781
|
+
throw new Error('Invalid user data');
|
|
782
|
+
}
|
|
783
|
+
return data;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
function parseConfig(configString: string): Config {
|
|
787
|
+
const parsed: unknown = JSON.parse(configString);
|
|
788
|
+
|
|
789
|
+
if (!isConfig(parsed)) {
|
|
790
|
+
throw new Error('Invalid config format');
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
return parsed;
|
|
794
|
+
}
|
|
795
|
+
```
|
|
796
|
+
|
|
797
|
+
### 3. ❌ Overly Complex Generic Chains
|
|
798
|
+
|
|
799
|
+
```typescript
|
|
800
|
+
// BAD: Generic inception (>3 levels deep)
|
|
801
|
+
type DeepGeneric<
|
|
802
|
+
T extends Record<string, unknown>,
|
|
803
|
+
K extends keyof T,
|
|
804
|
+
V extends T[K],
|
|
805
|
+
R extends V extends Array<infer U> ? U : never
|
|
806
|
+
> = R extends object ? Partial<R> : never;
|
|
807
|
+
|
|
808
|
+
// GOOD: Break into named steps
|
|
809
|
+
type ArrayElement<T> = T extends Array<infer U> ? U : never;
|
|
810
|
+
type ObjectOrNever<T> = T extends object ? T : never;
|
|
811
|
+
|
|
812
|
+
type ExtractedItem<T, K extends keyof T> = ObjectOrNever<ArrayElement<T[K]>>;
|
|
813
|
+
type PartialItem<T, K extends keyof T> = Partial<ExtractedItem<T, K>>;
|
|
814
|
+
|
|
815
|
+
// Or just use concrete types if only used once
|
|
816
|
+
```
|
|
817
|
+
|
|
818
|
+
### 4. ❌ Ignoring Strict Mode Errors
|
|
819
|
+
|
|
820
|
+
```typescript
|
|
821
|
+
// BAD: Disabling strict checks
|
|
822
|
+
{
|
|
823
|
+
"compilerOptions": {
|
|
824
|
+
"strict": false, // Defeats the purpose of TypeScript
|
|
825
|
+
"noImplicitAny": false
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
function doSomething(data) { // Implicit any
|
|
830
|
+
return data.value; // No safety
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
// GOOD: Enable strict mode and fix the issues
|
|
834
|
+
{
|
|
835
|
+
"compilerOptions": {
|
|
836
|
+
"strict": true
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
function doSomething(data: { value: string }): string {
|
|
841
|
+
return data.value;
|
|
842
|
+
}
|
|
843
|
+
```
|
|
844
|
+
|
|
845
|
+
### 5. ❌ Optional Properties Instead of Discriminated Unions
|
|
846
|
+
|
|
847
|
+
```typescript
|
|
848
|
+
// BAD: Ambiguous optional fields
|
|
849
|
+
type Result = {
|
|
850
|
+
success: boolean;
|
|
851
|
+
data?: User;
|
|
852
|
+
error?: string;
|
|
853
|
+
};
|
|
854
|
+
|
|
855
|
+
function handle(result: Result): void {
|
|
856
|
+
if (result.success) {
|
|
857
|
+
console.log(result.data.name); // TypeScript doesn't know data exists!
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
// GOOD: Discriminated union
|
|
862
|
+
type Result =
|
|
863
|
+
| { success: true; data: User }
|
|
864
|
+
| { success: false; error: string };
|
|
865
|
+
|
|
866
|
+
function handle(result: Result): void {
|
|
867
|
+
if (result.success) {
|
|
868
|
+
console.log(result.data.name); // TypeScript knows data exists
|
|
869
|
+
} else {
|
|
870
|
+
console.error(result.error); // TypeScript knows error exists
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
```
|
|
874
|
+
|
|
875
|
+
## Integration Notes
|
|
876
|
+
|
|
877
|
+
### Multi-Session Team Context
|
|
878
|
+
|
|
879
|
+
**If you are a worker in a multi-session team:**
|
|
880
|
+
|
|
881
|
+
1. **CRITICAL:** Call `team_check_inbox()` BEFORE starting work to receive shared artifacts and messages from other workers
|
|
882
|
+
2. **Read shared conventions:** Look for `shared-conventions` artifact containing naming conventions, enum values, response formats, error formats
|
|
883
|
+
3. **TypeScript applies to ALL workers:** Backend, frontend, testing workers all follow these patterns
|
|
884
|
+
4. **Publish shared types:** Create and publish `shared-types` artifact containing:
|
|
885
|
+
- Common interfaces (`User`, `ApiResponse<T>`, etc.)
|
|
886
|
+
- API request/response types
|
|
887
|
+
- Error types and error codes
|
|
888
|
+
- Enum/status value definitions
|
|
889
|
+
5. **Types = contract:** Types published in `shared-types` become the contract between frontend and backend workers — changes must be coordinated
|
|
890
|
+
6. **Broadcast completion:** Use `team_broadcast()` when you publish new types so other workers know to re-read the artifact
|
|
891
|
+
7. **Use relative paths only:** Never use absolute paths in published artifacts
|
|
892
|
+
|
|
893
|
+
### Artifact Publication Format
|
|
894
|
+
|
|
895
|
+
When publishing `shared-types` artifact:
|
|
896
|
+
|
|
897
|
+
```typescript
|
|
898
|
+
// shared-types artifact data structure
|
|
899
|
+
{
|
|
900
|
+
"files": ["src/types/user.types.ts", "src/types/api.types.ts"],
|
|
901
|
+
"exports": {
|
|
902
|
+
"user.types": ["User", "CreateUserRequest", "UpdateUserRequest"],
|
|
903
|
+
"api.types": ["ApiResponse", "ApiError", "Result"]
|
|
904
|
+
},
|
|
905
|
+
"conventions": {
|
|
906
|
+
"enumValues": {
|
|
907
|
+
"UserRole": ["admin", "user", "guest"],
|
|
908
|
+
"Status": ["pending", "active", "completed"]
|
|
909
|
+
},
|
|
910
|
+
"errorCodes": ["VALIDATION_ERROR", "NOT_FOUND", "UNAUTHORIZED"]
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
```
|
|
914
|
+
|
|
915
|
+
### Coordinating with Other Workers
|
|
916
|
+
|
|
917
|
+
- **Backend worker:** Consumes `shared-types` for request/response types, error handling
|
|
918
|
+
- **Frontend worker:** Consumes `shared-types` for API client types, state management
|
|
919
|
+
- **Testing worker:** Consumes `shared-types` for test data factories, type assertions
|
|
920
|
+
- **Database worker:** Shares model types via `shared-types` that backend worker uses
|
|
921
|
+
|
|
922
|
+
### Type Changes Workflow
|
|
923
|
+
|
|
924
|
+
1. Identify type change needed (e.g., add field to `User`)
|
|
925
|
+
2. Update type definition in `shared-types` artifact source
|
|
926
|
+
3. Re-publish `shared-types` artifact
|
|
927
|
+
4. Broadcast to team: "Updated User type — added `phoneNumber` field"
|
|
928
|
+
5. Affected workers re-read artifact and update their implementations
|
|
929
|
+
|
|
930
|
+
---
|
|
931
|
+
|
|
932
|
+
**Remember:** TypeScript is not just type checking — it's documentation, refactoring safety, and IDE autocomplete. Invest in types upfront, reap benefits throughout the project lifecycle.
|