Skip to main content

typescript

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

الانتقال إلى التثبيت

معلومات المصدر

المستودع
caidanw/skills
آخر نشاط في المصدر
١٠ أغسطس ٢٠٢٦ في ٢٣:٠٩
لغة SKILL.md المكتشفة
الإنجليزية
النجوم
٨
التفرعات
١

خيارات التثبيت

يُحدَّد Prompt الذي يراجع المصدر أولًا بشكل افتراضي. يمكنك التبديل إلى أمر مباشر أو تنزيل نسخة محلية.

مراجعة ملفات المصدر

اقرأ SKILL.md وأي ملفات مرافقة يعرضها SkillsMP قبل أن تقرر التثبيت.

مستكشف الملفات
5 ملفات

عرض SKILL.md

SKILL.md
تعليمات المصدر · معاينة للقراءة فقط
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.
عرض على GitHub
ملف SKILL.md هذا كبير جدا، لذلك يعرض SkillsMP القسم الاول فقط هنا. عرض على GitHub