@coralai/sps-cli 0.42.0 → 0.43.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 +34 -3
- package/dist/commands/projectInit.d.ts.map +1 -1
- package/dist/commands/projectInit.js +40 -53
- package/dist/commands/projectInit.js.map +1 -1
- package/dist/commands/skillCommand.d.ts +2 -0
- package/dist/commands/skillCommand.d.ts.map +1 -0
- package/dist/commands/skillCommand.js +235 -0
- package/dist/commands/skillCommand.js.map +1 -0
- package/dist/core/skillStore.d.ts +46 -0
- package/dist/core/skillStore.d.ts.map +1 -0
- package/dist/core/skillStore.js +197 -0
- package/dist/core/skillStore.js.map +1 -0
- package/dist/core/skillStore.test.d.ts +2 -0
- package/dist/core/skillStore.test.d.ts.map +1 -0
- package/dist/core/skillStore.test.js +190 -0
- package/dist/core/skillStore.test.js.map +1 -0
- package/dist/main.js +19 -17
- package/dist/main.js.map +1 -1
- package/package.json +1 -1
- package/skills/architecture-decision-records/SKILL.md +207 -0
- package/skills/backend/SKILL.md +62 -0
- package/skills/backend/references/api-design.md +168 -0
- package/skills/backend/references/caching.md +181 -0
- package/skills/backend/references/data-access.md +173 -0
- package/skills/backend/references/layering.md +181 -0
- package/skills/backend/references/observability.md +190 -0
- package/skills/backend/references/resilience.md +201 -0
- package/skills/backend/references/security.md +186 -0
- package/skills/backend-architect/SKILL.md +119 -0
- package/skills/code-reviewer/SKILL.md +143 -0
- package/skills/coding-standards/SKILL.md +60 -0
- package/skills/coding-standards/references/clean-code.md +258 -0
- package/skills/coding-standards/references/code-review.md +192 -0
- package/skills/coding-standards/references/commits-and-prs.md +226 -0
- package/skills/coding-standards/references/error-strategy.md +193 -0
- package/skills/coding-standards/references/naming.md +185 -0
- package/skills/coding-standards/references/tdd.md +171 -0
- package/skills/database/SKILL.md +53 -0
- package/skills/database/references/indexing.md +190 -0
- package/skills/database/references/migrations.md +199 -0
- package/skills/database/references/nosql.md +185 -0
- package/skills/database/references/queries.md +295 -0
- package/skills/database/references/scaling.md +203 -0
- package/skills/database/references/schema.md +191 -0
- package/skills/database-optimizer/SKILL.md +168 -0
- package/skills/debugging-workflow/SKILL.md +244 -0
- package/skills/devops/SKILL.md +55 -0
- package/skills/devops/references/ci-cd.md +204 -0
- package/skills/devops/references/containers.md +272 -0
- package/skills/devops/references/deploy.md +201 -0
- package/skills/devops/references/iac.md +252 -0
- package/skills/devops/references/observability.md +228 -0
- package/skills/devops/references/secrets.md +178 -0
- package/skills/devops-automator/SKILL.md +164 -0
- package/skills/frontend/SKILL.md +52 -0
- package/skills/frontend/references/accessibility.md +222 -0
- package/skills/frontend/references/components.md +206 -0
- package/skills/frontend/references/performance.md +219 -0
- package/skills/frontend/references/routing.md +209 -0
- package/skills/frontend/references/state.md +190 -0
- package/skills/frontend/references/testing.md +216 -0
- package/skills/frontend-developer/SKILL.md +115 -0
- package/skills/git-workflow/SKILL.md +355 -0
- package/skills/golang/SKILL.md +49 -0
- package/skills/golang/references/concurrency.md +284 -0
- package/skills/golang/references/errors.md +241 -0
- package/skills/golang/references/idioms.md +285 -0
- package/skills/golang/references/testing.md +238 -0
- package/skills/java/SKILL.md +50 -0
- package/skills/java/references/concurrency.md +194 -0
- package/skills/java/references/idioms.md +283 -0
- package/skills/java/references/testing.md +228 -0
- package/skills/kotlin/SKILL.md +47 -0
- package/skills/kotlin/references/coroutines.md +240 -0
- package/skills/kotlin/references/idioms.md +268 -0
- package/skills/kotlin/references/testing.md +219 -0
- package/skills/mobile/SKILL.md +50 -0
- package/skills/mobile/references/architecture.md +204 -0
- package/skills/mobile/references/navigation.md +158 -0
- package/skills/mobile/references/performance.md +152 -0
- package/skills/mobile/references/platform.md +166 -0
- package/skills/mobile/references/state-and-data.md +174 -0
- package/skills/python/SKILL.md +51 -0
- package/skills/python/THIRD_PARTY.md +14 -0
- package/skills/python/references/async.md +218 -0
- package/skills/python/references/error-handling.md +254 -0
- package/skills/python/references/idioms.md +279 -0
- package/skills/python/references/packaging.md +233 -0
- package/skills/python/references/testing.md +269 -0
- package/skills/python/references/typing.md +292 -0
- package/skills/qa-tester/SKILL.md +186 -0
- package/skills/rust/SKILL.md +50 -0
- package/skills/rust/references/async.md +224 -0
- package/skills/rust/references/errors.md +240 -0
- package/skills/rust/references/ownership.md +263 -0
- package/skills/rust/references/testing.md +274 -0
- package/skills/rust/references/traits.md +250 -0
- package/skills/security-engineer/SKILL.md +157 -0
- package/skills/swift/SKILL.md +48 -0
- package/skills/swift/references/concurrency.md +280 -0
- package/skills/swift/references/idioms.md +334 -0
- package/skills/swift/references/testing.md +229 -0
- package/skills/typescript/SKILL.md +51 -0
- package/skills/typescript/references/async.md +241 -0
- package/skills/typescript/references/errors.md +208 -0
- package/skills/typescript/references/idioms.md +246 -0
- package/skills/typescript/references/testing.md +225 -0
- package/skills/typescript/references/tooling.md +208 -0
- package/skills/typescript/references/types.md +259 -0
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
# TypeScript — Tooling
|
|
2
|
+
|
|
3
|
+
`tsconfig`, linting, formatting, bundlers, monorepos.
|
|
4
|
+
|
|
5
|
+
## `tsconfig.json` — the baseline
|
|
6
|
+
|
|
7
|
+
```json
|
|
8
|
+
{
|
|
9
|
+
"compilerOptions": {
|
|
10
|
+
"target": "ES2022",
|
|
11
|
+
"module": "ESNext",
|
|
12
|
+
"moduleResolution": "Bundler",
|
|
13
|
+
|
|
14
|
+
"strict": true,
|
|
15
|
+
"noUncheckedIndexedAccess": true,
|
|
16
|
+
"noImplicitOverride": true,
|
|
17
|
+
"exactOptionalPropertyTypes": true,
|
|
18
|
+
|
|
19
|
+
"esModuleInterop": true,
|
|
20
|
+
"skipLibCheck": true,
|
|
21
|
+
"forceConsistentCasingInFileNames": true,
|
|
22
|
+
|
|
23
|
+
"resolveJsonModule": true,
|
|
24
|
+
"isolatedModules": true,
|
|
25
|
+
|
|
26
|
+
"declaration": true,
|
|
27
|
+
"declarationMap": true,
|
|
28
|
+
"sourceMap": true,
|
|
29
|
+
|
|
30
|
+
"outDir": "dist",
|
|
31
|
+
"rootDir": "src"
|
|
32
|
+
},
|
|
33
|
+
"include": ["src/**/*"],
|
|
34
|
+
"exclude": ["**/*.test.ts", "dist"]
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Key switches:
|
|
39
|
+
|
|
40
|
+
| Option | Why |
|
|
41
|
+
|---|---|
|
|
42
|
+
| `strict: true` | Turns on all strict flags. Required. |
|
|
43
|
+
| `noUncheckedIndexedAccess` | `arr[0]` returns `T \| undefined`. Prevents off-by-one bugs. |
|
|
44
|
+
| `exactOptionalPropertyTypes` | `{ x?: number }` vs `{ x: number \| undefined }` are actually different. |
|
|
45
|
+
| `isolatedModules` | Required for swc/esbuild/bun. Catches things the single-file compilers can't handle. |
|
|
46
|
+
| `skipLibCheck: true` | Faster; trust your deps. |
|
|
47
|
+
|
|
48
|
+
## Node vs. bundler resolution
|
|
49
|
+
|
|
50
|
+
`moduleResolution: "Bundler"` for Vite, esbuild, webpack, Vite, Bun. `"NodeNext"` for plain Node.js. Pick based on the runtime.
|
|
51
|
+
|
|
52
|
+
## Path aliases
|
|
53
|
+
|
|
54
|
+
```json
|
|
55
|
+
{
|
|
56
|
+
"compilerOptions": {
|
|
57
|
+
"baseUrl": ".",
|
|
58
|
+
"paths": {
|
|
59
|
+
"@/*": ["src/*"]
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Mirror this in the bundler and test runner, or imports resolve in TS but not at runtime.
|
|
66
|
+
|
|
67
|
+
## Linting — ESLint (flat config)
|
|
68
|
+
|
|
69
|
+
```js
|
|
70
|
+
// eslint.config.js
|
|
71
|
+
import tseslint from 'typescript-eslint';
|
|
72
|
+
export default tseslint.config(
|
|
73
|
+
...tseslint.configs.recommendedTypeChecked,
|
|
74
|
+
{
|
|
75
|
+
languageOptions: { parserOptions: { project: true } },
|
|
76
|
+
rules: {
|
|
77
|
+
'@typescript-eslint/no-floating-promises': 'error',
|
|
78
|
+
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
|
|
79
|
+
'@typescript-eslint/switch-exhaustiveness-check': 'error',
|
|
80
|
+
'@typescript-eslint/consistent-type-imports': 'error',
|
|
81
|
+
'@typescript-eslint/no-explicit-any': 'error',
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
);
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
High-signal rules:
|
|
88
|
+
- `no-floating-promises` — forgotten `await` is a frequent bug
|
|
89
|
+
- `switch-exhaustiveness-check` — forgotten case in a union switch
|
|
90
|
+
- `no-misused-promises` — `async` callback passed where sync is expected
|
|
91
|
+
- `consistent-type-imports` — `import type { X }` keeps runtime lean
|
|
92
|
+
|
|
93
|
+
## Formatting — Prettier (or project-specific)
|
|
94
|
+
|
|
95
|
+
Let the formatter run in CI and on save. Don't argue style in PRs.
|
|
96
|
+
|
|
97
|
+
```json
|
|
98
|
+
// .prettierrc
|
|
99
|
+
{
|
|
100
|
+
"semi": true,
|
|
101
|
+
"singleQuote": true,
|
|
102
|
+
"trailingComma": "all",
|
|
103
|
+
"printWidth": 100,
|
|
104
|
+
"arrowParens": "always"
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Team preference is fine — the choice matters less than the consistency.
|
|
109
|
+
|
|
110
|
+
## `tsc` vs. the bundlers
|
|
111
|
+
|
|
112
|
+
| Tool | Role |
|
|
113
|
+
|---|---|
|
|
114
|
+
| `tsc` | Type checking + `.d.ts` emit |
|
|
115
|
+
| `esbuild` / `swc` | Fast transpile (strip types, downlevel syntax) |
|
|
116
|
+
| `tsup` / `rollup` | Libraries with exports maps |
|
|
117
|
+
| `vite` / `rsbuild` / `rspack` | App bundling (frontend / dev server) |
|
|
118
|
+
|
|
119
|
+
Typical modern setup: `vitest` for tests, `vite` for the dev server, `tsc --noEmit` in CI for type checking. Transpile is handled by the bundler — `tsc` is not on the hot path.
|
|
120
|
+
|
|
121
|
+
## Package managers
|
|
122
|
+
|
|
123
|
+
`pnpm` is the default recommendation:
|
|
124
|
+
- Content-addressable store → disk savings, fast installs
|
|
125
|
+
- Strict by default → catches phantom dependencies
|
|
126
|
+
- Excellent monorepo support via workspaces
|
|
127
|
+
|
|
128
|
+
`npm` works. `yarn classic` (v1) is deprecated; `yarn berry` (v3+) is fine but niche. `bun` is fastest but still rough around the edges for some toolchains.
|
|
129
|
+
|
|
130
|
+
Commit the lockfile.
|
|
131
|
+
|
|
132
|
+
## Monorepo
|
|
133
|
+
|
|
134
|
+
For 3+ packages that share code, use workspaces.
|
|
135
|
+
|
|
136
|
+
```json
|
|
137
|
+
// package.json (root)
|
|
138
|
+
{
|
|
139
|
+
"private": true,
|
|
140
|
+
"workspaces": ["packages/*", "apps/*"]
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Prefer workspace tools over hand-rolled symlinks:
|
|
145
|
+
- `pnpm` workspaces (native)
|
|
146
|
+
- `turborepo` / `nx` for task orchestration + caching
|
|
147
|
+
- `changesets` for versioning + release notes
|
|
148
|
+
|
|
149
|
+
Keep the dependency graph explicit — `apps/web` depends on `packages/ui`, not the reverse.
|
|
150
|
+
|
|
151
|
+
## `package.json` essentials
|
|
152
|
+
|
|
153
|
+
```json
|
|
154
|
+
{
|
|
155
|
+
"name": "@org/pkg",
|
|
156
|
+
"version": "1.2.0",
|
|
157
|
+
"type": "module",
|
|
158
|
+
"exports": {
|
|
159
|
+
".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" },
|
|
160
|
+
"./utils": { "types": "./dist/utils.d.ts", "import": "./dist/utils.js" }
|
|
161
|
+
},
|
|
162
|
+
"files": ["dist"],
|
|
163
|
+
"scripts": {
|
|
164
|
+
"build": "tsup src/index.ts --format esm --dts",
|
|
165
|
+
"test": "vitest run",
|
|
166
|
+
"lint": "eslint .",
|
|
167
|
+
"typecheck": "tsc --noEmit"
|
|
168
|
+
},
|
|
169
|
+
"engines": { "node": ">=20" }
|
|
170
|
+
}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
- `type: "module"` — ESM by default. New projects should be ESM.
|
|
174
|
+
- `exports` — explicit public API, not "everything in the dist directory".
|
|
175
|
+
- `engines` — fail loudly when users install on an unsupported Node.
|
|
176
|
+
|
|
177
|
+
## Dev vs. prod dependencies
|
|
178
|
+
|
|
179
|
+
- `dependencies`: anything the shipped app actually runs
|
|
180
|
+
- `devDependencies`: build tools, test frameworks, types
|
|
181
|
+
- `peerDependencies`: libraries (only) — "I need X, user provides it"
|
|
182
|
+
|
|
183
|
+
A library that ships `react` in `dependencies` causes duplicate-React bugs. Put it in `peerDependencies` with a loose range.
|
|
184
|
+
|
|
185
|
+
## CI checklist
|
|
186
|
+
|
|
187
|
+
```yaml
|
|
188
|
+
# GitHub Actions sketch
|
|
189
|
+
- run: pnpm install --frozen-lockfile
|
|
190
|
+
- run: pnpm typecheck
|
|
191
|
+
- run: pnpm lint
|
|
192
|
+
- run: pnpm test --coverage
|
|
193
|
+
- run: pnpm build
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
Order matters: typecheck before tests (cheap fail); lint before tests (cheap fail); build last.
|
|
197
|
+
|
|
198
|
+
## Anti-patterns
|
|
199
|
+
|
|
200
|
+
| Anti-pattern | Fix |
|
|
201
|
+
|---|---|
|
|
202
|
+
| `strict: false` | Turn it on; fix errors incrementally with `// @ts-expect-error` |
|
|
203
|
+
| `tsc` on every file save in a large repo | Use `--incremental` or IDE's language server |
|
|
204
|
+
| Leaving `skipLibCheck: false` | Fine for a tiny project; wastes CI on any larger one |
|
|
205
|
+
| Committing `dist/` | Publish it, don't commit it |
|
|
206
|
+
| `type: "commonjs"` in new projects | Go ESM |
|
|
207
|
+
| Mixing formatters across directories | One config at the repo root |
|
|
208
|
+
| `ts-node` in production | Pre-compile; don't transpile at boot |
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
# TypeScript — Types
|
|
2
|
+
|
|
3
|
+
Generics, unions, discriminated unions, utility types, brands.
|
|
4
|
+
|
|
5
|
+
## `unknown` over `any`
|
|
6
|
+
|
|
7
|
+
`any` disables type checking. `unknown` forces you to narrow before use.
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
// ❌ silent bugs
|
|
11
|
+
function parse(raw: any) {
|
|
12
|
+
return raw.user.email.toLowerCase(); // crashes at runtime, no TS error
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// ✅
|
|
16
|
+
function parse(raw: unknown) {
|
|
17
|
+
if (typeof raw === 'object' && raw !== null && 'user' in raw) {
|
|
18
|
+
// ... still need more narrowing
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// ✅✅ parse at boundary with a schema
|
|
23
|
+
import { z } from 'zod';
|
|
24
|
+
const Schema = z.object({ user: z.object({ email: z.string().email() }) });
|
|
25
|
+
function parse(raw: unknown) {
|
|
26
|
+
const { user } = Schema.parse(raw); // throws on bad shape
|
|
27
|
+
return user.email.toLowerCase();
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Discriminated unions — the TS superpower
|
|
32
|
+
|
|
33
|
+
Model state with a tagged sum type. Narrowing happens automatically.
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
type Result<T, E> =
|
|
37
|
+
| { ok: true; value: T }
|
|
38
|
+
| { ok: false; error: E };
|
|
39
|
+
|
|
40
|
+
function handle(r: Result<User, Error>) {
|
|
41
|
+
if (r.ok) {
|
|
42
|
+
// r is narrowed to { ok: true; value: User }
|
|
43
|
+
console.log(r.value.name);
|
|
44
|
+
} else {
|
|
45
|
+
// r is narrowed to { ok: false; error: Error }
|
|
46
|
+
console.error(r.error.message);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Prefer over boolean flags + optional fields:
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
// ❌
|
|
55
|
+
type Loading = { isLoading: boolean; data?: User; error?: string };
|
|
56
|
+
|
|
57
|
+
// ✅
|
|
58
|
+
type Loading =
|
|
59
|
+
| { status: 'idle' }
|
|
60
|
+
| { status: 'loading' }
|
|
61
|
+
| { status: 'success'; data: User }
|
|
62
|
+
| { status: 'error'; error: string };
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
The compiler stops you from accessing `data` on an `error` state.
|
|
66
|
+
|
|
67
|
+
## Exhaustiveness checks
|
|
68
|
+
|
|
69
|
+
When you switch over a union, make sure every case is handled.
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
function render(r: Loading) {
|
|
73
|
+
switch (r.status) {
|
|
74
|
+
case 'idle': return <Idle />;
|
|
75
|
+
case 'loading': return <Spinner />;
|
|
76
|
+
case 'success': return <Show data={r.data} />;
|
|
77
|
+
case 'error': return <Err msg={r.error} />;
|
|
78
|
+
default: return assertNever(r);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function assertNever(x: never): never {
|
|
83
|
+
throw new Error(`Unreachable: ${JSON.stringify(x)}`);
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Add a new variant → the compiler forces you to update every `switch`.
|
|
88
|
+
|
|
89
|
+
## Generics
|
|
90
|
+
|
|
91
|
+
Use when a function / class preserves a type relationship.
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
function first<T>(xs: readonly T[]): T | undefined {
|
|
95
|
+
return xs[0];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Bounded
|
|
99
|
+
function byId<T extends { id: string }>(xs: T[], id: string): T | undefined {
|
|
100
|
+
return xs.find(x => x.id === id);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Default
|
|
104
|
+
type Paginated<T, Cursor = string> = {
|
|
105
|
+
data: T[];
|
|
106
|
+
nextCursor: Cursor | null;
|
|
107
|
+
};
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
If a generic parameter appears only once in the signature, you probably don't need generics.
|
|
111
|
+
|
|
112
|
+
## Utility types — the greatest hits
|
|
113
|
+
|
|
114
|
+
| Type | Use |
|
|
115
|
+
|---|---|
|
|
116
|
+
| `Partial<T>` | All fields optional |
|
|
117
|
+
| `Required<T>` | All fields required |
|
|
118
|
+
| `Readonly<T>` | All fields readonly |
|
|
119
|
+
| `Pick<T, K>` | Subset of fields |
|
|
120
|
+
| `Omit<T, K>` | All fields except K |
|
|
121
|
+
| `Record<K, V>` | Map-like object |
|
|
122
|
+
| `Awaited<P>` | Unwrap Promise |
|
|
123
|
+
| `ReturnType<F>` | The return type of a function |
|
|
124
|
+
| `Parameters<F>` | Tuple of function params |
|
|
125
|
+
| `NonNullable<T>` | Exclude null / undefined |
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
type User = { id: string; email: string; password: string };
|
|
129
|
+
type PublicUser = Omit<User, 'password'>;
|
|
130
|
+
type UserUpdate = Partial<Pick<User, 'email' | 'password'>>;
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## Branded types
|
|
134
|
+
|
|
135
|
+
Prevent string / number mix-ups at compile time.
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
type Brand<K, T> = K & { __brand: T };
|
|
139
|
+
|
|
140
|
+
type UserId = Brand<string, 'UserId'>;
|
|
141
|
+
type OrgId = Brand<string, 'OrgId'>;
|
|
142
|
+
|
|
143
|
+
function userId(s: string): UserId { return s as UserId; }
|
|
144
|
+
function orgId(s: string): OrgId { return s as OrgId; }
|
|
145
|
+
|
|
146
|
+
function findUser(id: UserId): User { ... }
|
|
147
|
+
|
|
148
|
+
findUser(userId('u_1')); // ✅
|
|
149
|
+
findUser('u_1'); // ❌ not a UserId
|
|
150
|
+
findUser(orgId('o_1')); // ❌ wrong brand
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
The runtime cost is zero — it's just a compile-time distinction. Use it for domain ids, units, hashed vs raw strings.
|
|
154
|
+
|
|
155
|
+
## Type guards
|
|
156
|
+
|
|
157
|
+
Custom predicates that narrow.
|
|
158
|
+
|
|
159
|
+
```ts
|
|
160
|
+
function isUser(x: unknown): x is User {
|
|
161
|
+
return typeof x === 'object' && x !== null && 'id' in x && typeof (x as any).id === 'string';
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const raw: unknown = fetchSomething();
|
|
165
|
+
if (isUser(raw)) {
|
|
166
|
+
raw.id.toLowerCase(); // raw is narrowed to User
|
|
167
|
+
}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
For anything more than two fields, use a schema validator (`zod`, `valibot`). Hand-written guards drift from the shape.
|
|
171
|
+
|
|
172
|
+
## `as const` — literal-typed values
|
|
173
|
+
|
|
174
|
+
Widening turns `"hello"` into `string`. `as const` keeps it literal.
|
|
175
|
+
|
|
176
|
+
```ts
|
|
177
|
+
const ROLES = ['admin', 'user', 'guest'] as const;
|
|
178
|
+
type Role = typeof ROLES[number]; // 'admin' | 'user' | 'guest'
|
|
179
|
+
|
|
180
|
+
const CONFIG = { retries: 3, timeout: 5000 } as const;
|
|
181
|
+
// CONFIG.retries is 3, not number; CONFIG is readonly
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
## Conditional & mapped types — use sparingly
|
|
185
|
+
|
|
186
|
+
Powerful, but expensive on the reader. Reach for them when the alternative is copy-paste.
|
|
187
|
+
|
|
188
|
+
```ts
|
|
189
|
+
// Conditional
|
|
190
|
+
type NonNull<T> = T extends null | undefined ? never : T;
|
|
191
|
+
|
|
192
|
+
// Mapped
|
|
193
|
+
type Nullable<T> = { [K in keyof T]: T[K] | null };
|
|
194
|
+
|
|
195
|
+
// Both, with `infer`
|
|
196
|
+
type UnwrapArray<T> = T extends (infer U)[] ? U : T;
|
|
197
|
+
|
|
198
|
+
type X = UnwrapArray<User[]>; // User
|
|
199
|
+
type Y = UnwrapArray<string>; // string
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
If the type gets fancy enough that a teammate asks "what does this do?", add a comment with an example, or simplify.
|
|
203
|
+
|
|
204
|
+
## `never` — the bottom type
|
|
205
|
+
|
|
206
|
+
`never` appears when TS knows no value can reach here.
|
|
207
|
+
|
|
208
|
+
```ts
|
|
209
|
+
function throwErr(m: string): never { throw new Error(m); }
|
|
210
|
+
|
|
211
|
+
const x = cond ? 1 : throwErr('no'); // x is number
|
|
212
|
+
|
|
213
|
+
// Exhaustiveness (see above)
|
|
214
|
+
default: return assertNever(r);
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
`never` in a position where you expect a value is a bug signal: "you forgot a case".
|
|
218
|
+
|
|
219
|
+
## `satisfies` — typecheck without widening
|
|
220
|
+
|
|
221
|
+
Force a value to conform to a type, but keep its narrow inferred type.
|
|
222
|
+
|
|
223
|
+
```ts
|
|
224
|
+
const routes = {
|
|
225
|
+
home: '/home',
|
|
226
|
+
profile: '/profile/:id',
|
|
227
|
+
} satisfies Record<string, string>;
|
|
228
|
+
|
|
229
|
+
// routes.home is the literal "/home", not string
|
|
230
|
+
type RouteKey = keyof typeof routes; // 'home' | 'profile'
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
Great for config objects. `as Record<string, string>` would erase the literals.
|
|
234
|
+
|
|
235
|
+
## Enums — avoid; prefer string unions
|
|
236
|
+
|
|
237
|
+
```ts
|
|
238
|
+
// ❌ enum (generates runtime code, not tree-shakeable, weird reverse mapping)
|
|
239
|
+
enum Role { Admin, User, Guest }
|
|
240
|
+
|
|
241
|
+
// ✅ string union (zero runtime, tree-shakeable, grepable)
|
|
242
|
+
type Role = 'admin' | 'user' | 'guest';
|
|
243
|
+
|
|
244
|
+
// ✅ object + `as const` if you need a value container
|
|
245
|
+
const Role = { Admin: 'admin', User: 'user', Guest: 'guest' } as const;
|
|
246
|
+
type Role = typeof Role[keyof typeof Role];
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
`const enum` exists but trips up bundlers and project-references. Not worth it.
|
|
250
|
+
|
|
251
|
+
## Class vs. type vs. interface
|
|
252
|
+
|
|
253
|
+
| Use | When |
|
|
254
|
+
|---|---|
|
|
255
|
+
| `type` | Unions, intersections, mapped / conditional, brands, non-extensible shapes |
|
|
256
|
+
| `interface` | Public object contract, open for extension by users (declaration merging) |
|
|
257
|
+
| `class` | Need a runtime object with methods and state |
|
|
258
|
+
|
|
259
|
+
Prefer `type` for data and `interface` for object behaviours. Don't mix conventions arbitrarily.
|