بنقرة واحدة
typescript
Write clean, pragmatically functional TypeScript — simple, composable, soundly typed
التثبيت باستخدام Codex أو Claude انسخ هذا Prompt والصقه في Codex أو Claude أو مساعد آخر ليراجع صفحة Skill ويثبّتها لك.
القائمة
Write clean, pragmatically functional TypeScript — simple, composable, soundly typed
التثبيت باستخدام Codex أو Claude انسخ هذا Prompt والصقه في Codex أو Claude أو مساعد آخر ليراجع صفحة Skill ويثبّتها لك.
استنادا إلى تصنيف SOC المهني
Adversarially hunt for correctness bugs and regressions in a change set. Use when reviewing a diff before finalizing, during a cleanup pass, or when asked to find bugs, pressure-test, or stress-test recently modified code. Triggers on: "find bugs", "what could break", "review for correctness", "did I break anything", "hunt regressions", "adversarial review", "pressure-test this change".
End-of-session cleanup pass for Swift code. Use this skill whenever the user asks to "clean up", "polish", "tidy", "refactor", or "review" Swift work at the end of a coding session, or when they invoke this skill by name. Performs a holistic review of files modified during the session — simplification, type-driven design, macOS conventions, and comment hygiene. Make sure to use this skill anytime the user signals they want to wrap up or finalize Swift work, even if they don't explicitly say "cleanup". Do NOT use for one-off edits, bug fixes, or active feature work.
End-of-session cleanup pass for TypeScript, React, and web code. Use this skill whenever the user asks to "clean up", "polish", "tidy", "refactor", or "review" web/frontend work at the end of a coding session, or when they invoke this skill by name. Performs a holistic review of files modified during the session — simplification, type safety, React patterns, CSS modernization, and comment hygiene. Make sure to use this skill anytime the user signals they want to wrap up or finalize TypeScript/React/web work, even if they don't explicitly say "cleanup". Do NOT use for one-off edits, bug fixes, or active feature work.
Simplify and refine recently modified code for clarity and consistency. Use after writing or modifying code, during cleanup passes, or when asked to simplify/refactor without changing behavior.
Pull PR review comments and triage them — separate substantive feedback from bikeshedding, stale comments, misreads, AI slop, and other noise. Use this skill whenever the user asks to "review my PR comments", "triage feedback", "go through my PR", or wants help deciding what to address vs push back on. Make sure to use this skill anytime the user mentions PR comments or code review feedback, even if they don't explicitly say "triage". Pulls comments via the `gh` CLI, classifies each, and proposes an action with a draft response. Do NOT use for writing PR descriptions, reviewing someone else's PR, or applying the changes — this skill triages and drafts only.
The 23 Gang of Four object-oriented design patterns (Gamma, Helm, Johnson, Vlissides, 1994) distilled as a practical field guide, not a catalog. Use when naming a shape in code review, choosing between competing designs, deciding whether to introduce indirection, or auditing for over-abstraction. Each pattern documents intent, tradeoffs, modern relevance, language-level replacements, and when NOT to use it. Triggers on: "design pattern", "GoF", "factory", "builder", "singleton", "adapter", "decorator", "observer", "strategy", "visitor", "state machine", "command pattern", "too many constructors", "subclass explosion", "program to an interface", "composition over inheritance", "am I over-abstracting this", "Swift design patterns", "protocol-oriented programming", "POP in Swift", "Crusty talk", "protocol vs class", "value types over reference types", "start with a protocol".
| name | TypeScript |
| description | Write clean, pragmatically functional TypeScript — simple, composable, soundly typed |
| globs | ["*.ts","*.tsx","*.mts","*.cts"] |
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.
Choose clarity over brevity. Explicit code that's easy to scan beats compact code that requires careful parsing.
// 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.
// 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 />
Functions should do one thing and be easy to combine. Name them so the call site reads naturally.
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.
Functions that take or return functions. Use them naturally — they're just functions.
// 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.
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.
// 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.
interface AppConfig {
readonly apiUrl: string
readonly features: readonly string[]
}
This is the single most valuable TypeScript pattern. Model your domain with discriminated unions so impossible states can't exist.
// 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:
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.
Validate and parse data when it enters your system. After that, trust the types. Don't scatter validation checks through business logic.
// 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.
// 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]
}
When you can't make an illegal state structurally impossible, use branded types with constructors:
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 { /* ... */ }
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.
// 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.
// 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:
// 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)
}
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.
// 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)
Functions that return their argument unchanged — their only job is type inference:
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[]
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.
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")
}
}
Always type catch block errors as unknown, then narrow. Use Error.cause for chaining context through layers.
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.
One type for lazy/async/sync values — useful for config, subcommands, anything expensive:
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
}
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).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."cond ? "a" : "b" infers as string, not "a" | "b". Use as const on the branches if you need literals.{ compare(a: T): number } skips contravariance checks. Use function property syntax { compare: (a: T) => number } for type safety under strictFunctionTypes.enum — use union types or as const objects. Enums emit runtime code (non-erasable syntax), break Node's native TS support, and conflict with the "types as comments" future. TS 5.8's --erasableSyntaxOnly flag officially marks them as discouraged.namespace — use modules. Also non-erasable.I prefix on interfaces — just name the thing.ts-pattern).createGenericHandler.any — use unknown and narrow, or fix the type.const everywhere. let only for genuine reassignment.type by default. interface only for extends. (See Section 7.)({ id, name }: User) => ...user?.address?.city ?? "Unknown"function keyword for named, exported functions. Arrows for inline callbacks and short helpers.as const to preserve literal types.satisfies to validate shape while preserving inference.noUncheckedIndexedAccess — makes array/object index access return T | undefined, catching unsafe assumptions like arr[0] or env.NODE_ENV.--erasableSyntaxOnly — disables enums, namespaces, and parameter properties. Aligns with Node's native TS support and the "types as comments" TC39 proposal.For modern TypeScript features (using, NoInfer, const type params), see references/modern-features.md.
For compilation performance tips, see references/performance.md.
For patterns from top codebases (Zod, tRPC, Hono, TanStack), see references/patterns-in-the-wild.md.