Skip to main content

typescript

Write clean, pragmatically functional TypeScript — simple, composable, soundly typed

Ir para a instalação

Informações da origem

Repositório
caidanw/skills
Última atividade na origem
10 de agosto de 2026 às 23:09
Idioma detectado do SKILL.md
inglês
Estrelas
8
Forks
1

Opções de instalação

Por padrão, está selecionado o prompt que primeiro revisa a origem. Você pode mudar para um comando direto ou baixar uma cópia local.

Revise os arquivos de origem

Leia o SKILL.md e os arquivos complementares exibidos pelo SkillsMP antes de decidir se vai instalar.

Explorador de arquivos
5 arquivos

Exibindo SKILL.md

SKILL.md
Instruções da origem · Visualização somente leitura
name
typescript
description
Write clean, pragmatically functional TypeScript — simple, composable, soundly typed
globs
["*.ts","*.tsx","*.mts","*.cts"]
# Pragmatic TypeScript Write TypeScript that is **simple, clear, composable, and soundly typed**. Use the good parts of functional style — small functions, higher-order functions, discriminated unions, composition — without dogma. Mutate when it's simpler. Be explicit, not clever. Prioritize readability over density. ## 1. Simple and Clear Choose clarity over brevity. Explicit code that's easy to scan beats compact code that requires careful parsing. ```ts // Good — clear, easy to follow const activeUsers = users.filter(u => u.status === "active") const emails = activeUsers.map(u => u.email) // Fine too — chained when each step is obvious const emails = users .filter(u => u.status === "active") .map(u => u.email) // Bad — too dense, hard to debug const emails = users.reduce((acc, u) => u.status === "active" ? [...acc, u.email] : acc, [] as string[]) ``` Avoid nested ternaries. Use `switch`, `if/else`, or early returns for multiple conditions. ```ts // Good switch (status) { case "idle": return null case "loading": return <Spinner /> case "error": return <ErrorBanner error={req.error} /> case "success": return <Data value={req.data} /> } // Bad return status === "idle" ? null : status === "loading" ? <Spinner /> : status === "error" ? <ErrorBanner /> : <Data /> ``` ## 2. Small Functions That Compose Functions should do one thing and be easy to combine. Name them so the call site reads naturally. ```ts const isActive = (u: User) => u.status === "active" const byCreatedDesc = (a: User, b: User) => b.createdAt - a.createdAt const recentActive = users .filter(isActive) .sort(byCreatedDesc) .slice(0, 10) ``` Extract predicates, comparators, and mappers when they're reused or when inlining them hurts readability. Don't extract trivial one-offs — `u => u.id` is fine inline. Keep utility functions in the module that uses them. Only pull them out to a shared file when a second consumer actually appears. ## 3. Higher-Order Functions Functions that take or return functions. Use them naturally — they're just functions. ```ts // A function that returns a function function createValidator<T>(rules: ValidationRule<T>[]) { return function validate(value: T): string[] { return rules .map(rule => rule.check(value) ? null : rule.message) .filter((msg): msg is string => msg !== null) } } // A function that wraps another function function withRetry<T>(fn: () => Promise<T>, attempts = 3): Promise<T> { return fn().catch(err => attempts > 1 ? withRetry(fn, attempts - 1) : Promise.reject(err) ) } ``` Keep the types readable. If a generic signature is getting gnarly, break it up — name intermediate types, use interfaces, add a comment. Don't make people squint. ## 4. Mutate When It's Simpler Don't create new objects for the sake of "immutability." TypeScript creates a lot of garbage when you spread compulsively. Mutate local state freely. Use `readonly` at API boundaries and shared state, not on every field of every type. ```ts // Good — mutating a local array is fine function buildIndex(items: Item[]): Map<string, Item> { const index = new Map<string, Item>() for (const item of items) { index.set(item.id, item) } return index } // Good — spreading is fine when it's simple const updated = { ...user, name: newName } // Bad — spreading inside a loop creating tons of intermediate objects const result = items.reduce((acc, item) => ({ ...acc, [item.id]: item }), {}) ``` **The real test: does the mutation escape the current scope?** Mutating a local variable inside a function is always fine. Mutating shared state or arguments passed in is almost always a bug waiting to happen. Use `readonly` where it prevents real bugs — public API return types, shared config, state that shouldn't be touched. ```ts interface AppConfig { readonly apiUrl: string readonly features: readonly string[] } ``` ## 5. Make Illegal States Unrepresentable This is the single most valuable TypeScript pattern. Model your domain with discriminated unions so impossible states can't exist. ```ts // Good — each state carries exactly the data it needs type AsyncState<T> = | { status: "idle" } | { status: "loading"; requestId: string } | { status: "success"; data: T } | { status: "error"; error: Error } // Bad — boolean soup, impossible combinations representable interface AsyncState<T> { isLoading?: boolean isError?: boolean data?: T error?: Error requestId?: string } ``` If you `switch` on a discriminant at runtime, your types should mirror that structure. Use `never` in the default to ensure exhaustive handling — the compiler catches missing cases: ```ts type Shape = | { kind: "circle"; radius: number } | { kind: "rect"; width: number; height: number } function assertNever(x: never): never { throw new Error(`Unexpected value: ${x}`) } function area(s: Shape): number { switch (s.kind) { case "circle": return Math.PI * s.radius ** 2 case "rect": return s.width * s.height default: return assertNever(s) // compile error if a case is missing } } ``` Use this for: request states, auth states, form steps, permissions, payment states, message types — anything with distinct modes. ## 6. Parse at the Boundary Validate and parse data when it enters your system. After that, trust the types. Don't scatter validation checks through business logic. ```ts // Parse once at the boundary function parseUser(data: unknown): User { if (!isObject(data)) throw new ParseError("expected object") const name = parseString(data.name, "name") const email = parseEmail(data.email) const age = parseAge(data.age) return { name, email, age } } // Business logic trusts the types — no re-validation function greetUser(user: User): string { return `Hello, ${user.name}!` } ``` **Strengthen inputs, don't weaken outputs.** If a function needs a non-empty array, require `[T, ...T[]]` instead of accepting `T[]` and returning `T | undefined`. ```ts // Prefer this — caller proves the precondition function first<T>(list: [T, ...T[]]): T { return list[0] } // Over this — pushes uncertainty downstream function first<T>(list: T[]): T | undefined { return list[0] } ``` ### Branded Types When you can't make an illegal state structurally impossible, use branded types with constructors: ```ts type EmailAddress = string & { readonly __brand: "EmailAddress" } type UserId = string & { readonly __brand: "UserId" } function EmailAddress(input: string): EmailAddress { if (!input.includes("@")) throw new Error(`Invalid email: ${input}`) return input as EmailAddress } function UserId(input: string): UserId { if (!input.trim()) throw new Error("UserId cannot be empty") return input as UserId } // Now these are distinct types — can't accidentally swap them function sendEmail(to: EmailAddress, body: string): void { /* ... */ } ``` ## 7. Types: Precise but Simple Make types tight — describe exactly what's possible, nothing more. But don't over-engineer the type system. If a type is hard to read, simplify it. ```ts // Good — narrow string literals type Method = "GET" | "POST" | "PUT" | "DELETE" type Status = "active" | "inactive" | "pending" // Good — satisfies preserves literal types while validating const routes = { home: "/", about: "/about", user: "/user/:id", } satisfies Record<string, string> // Good — simple generics with clear constraints function pick<T, K extends keyof T>(obj: T, keys: K[]): Pick<T, K> { const result = {} as Pick<T, K> for (const key of keys) result[key] = obj[key] return result } // Bad — type gymnastics that nobody can read type DeepPartialConditionalMappedInferredNested<T> = ... ``` Default to `type`. Use `interface` when you need `extends` — it creates cached, flat object types that the compiler checks faster than `&` intersections. Avoid `interface` as a default because declaration merging (two interfaces with the same name silently merge) causes surprising bugs. ```ts // type by default — for objects, unions, aliases, computed types type User = { id: UserId name: string email: EmailAddress status: Status } type AsyncResult<T> = AsyncState<T> type UserInput = Omit<User, "id"> type Handler = (req: Request) => Promise<Response> // interface when you need extends — faster than & intersections interface HttpError extends Error { status: number body: unknown } ``` Use `unknown` over `any`. Narrow with type guards: ```ts // Type predicates — narrow in .filter(), if blocks, etc. function isString(val: unknown): val is string { return typeof val === "string" } const strings = mixed.filter(isString) // string[] // Assertion functions — narrow or throw function assertDefined<T>(val: T | undefined, msg: string): asserts val is T { if (val === undefined) throw new Error(msg) } ``` ## 8. Factory Functions by Default, Classes When Earned Use factory functions with closures for configurable, composable objects. Reserve classes for: wrapping resources (DB connections, WebSocket, streams), fluent/chainable APIs (builders, schema validators like Zod), and `Disposable` objects used with `using`. ```ts // Factory — simple, composable, no `this` headaches function createFetcher(defaults: FetchOptions = {}) { async function request<T>(url: string, opts: FetchOptions = {}): Promise<T> { const merged = { ...defaults, ...opts } const res = await fetch(url, merged) if (!res.ok) throw new HttpError(res.status, await res.text()) return res.json() } // Composable — create specialized fetchers request.withAuth = (token: string) => createFetcher({ ...defaults, headers: { ...defaults.headers, Authorization: `Bearer ${token}` } }) return request } const api = createFetcher({ baseUrl: "https://api.example.com" }) const authed = api.withAuth(token) ``` ### Identity Functions for Type Inference Functions that return their argument unchanged — their only job is type inference: ```ts function defineConfig<const T extends AppConfig>(config: T): T { return config } // The `const` modifier preserves literal types const config = defineConfig({ routes: ["/api/users", "/api/posts"], features: ["auth", "billing"], }) // Type preserves the literal string arrays, not just string[] ``` ## 9. Useful Patterns ### Result Type (When Appropriate) For expected failures where you want the caller to handle both paths explicitly. Don't use this everywhere — `throw` is fine for truly exceptional errors. ```ts type Result<T, E = Error> = | { ok: true; value: T } | { ok: false; error: E } function ok<T>(value: T): Result<T, never> { return { ok: true, value } } function err<E>(error: E): Result<never, E> { return { ok: false, error } } function parseConfig(raw: string): Result<Config, string> { try { const parsed = JSON.parse(raw) if (!parsed.host) return err("missing host") return ok(parsed as Config) } catch { return err("invalid JSON") } } ``` ### Error Handling Always type catch block errors as `unknown`, then narrow. Use `Error.cause` for chaining context through layers. ```ts try { await fetchData() } catch (err) { if (err instanceof Error) { throw new Error("Failed to load data", { cause: err }) } throw err } ``` Use Result types for expected failures (Section 9 above). Use `throw` for truly unexpected errors. Don't mix — pick one strategy per boundary. ### Resolvable Values One type for lazy/async/sync values — useful for config, subcommands, anything expensive: ```ts type Resolvable<T> = T | Promise<T> | (() => T) | (() => Promise<T>) async function resolve<T>(input: Resolvable<T>): Promise<T> { return typeof input === "function" ? (input as Function)() : input } ``` ## 10. Common Gotchas Concrete pitfalls that cause runtime bugs despite passing type checks: - **`Object.keys()` returns `string[]`**, not `(keyof T)[]`. TypeScript can't guarantee an object doesn't have extra keys at runtime. Cast explicitly when safe: `Object.keys(obj) as Array<keyof typeof obj>`. - **`.filter()` doesn't narrow** without a type predicate. Use `.filter((x): x is T => x !== null)` instead of `.filter(x => x !== null)`. - **Catch blocks: errors are `unknown`**, not `Error`. Always narrow: `catch (err) { if (err instanceof Error) ... }`. - **`{}` matches any non-nullish value** — including strings, numbers, and booleans. Use `Record<string, unknown>` for "some object" and `unknown` for "anything." - **Type widening in conditionals**: `cond ? "a" : "b"` infers as `string`, not `"a" | "b"`. Use `as const` on the branches if you need literals.
Ver no GitHub
Este SKILL.md e muito grande, entao o SkillsMP mostra aqui apenas a primeira secao. Ver no GitHub