$npx -y skills add jgamaraalv/ts-dev-kit --skill typescript-conventionsTypeScript coding conventions for strict, type-safe projects. Use when: (1) writing or reviewing TypeScript code, (2) choosing between any and unknown, (3) structuring imports with verbatimModuleSyntax, (4) defining types, interfaces, unions, or generics, (5) naming functions
| 1 | # TypeScript Conventions |
| 2 | |
| 3 | Project-wide TypeScript standards that complement agent-specific instructions. |
| 4 | |
| 5 | <rules> |
| 6 | |
| 7 | ## Type Safety |
| 8 | |
| 9 | - **No `any`**: Use `unknown` if the type is truly dynamic, then narrow. |
| 10 | - **Strict config**: `strict: true`, `noUncheckedIndexedAccess`, `verbatimModuleSyntax`. |
| 11 | - Use `Readonly<T>`, `Pick`, `Omit`, and `Record` for precise types. |
| 12 | - Use branded types for entity IDs (e.g., `UserId`, `OrderId`) to prevent mixing. |
| 13 | - Prefer `z.infer<typeof schema>` over hand-written types when a Zod schema exists. |
| 14 | |
| 15 | ## Interface vs Type |
| 16 | |
| 17 | - **Interfaces** for object shapes that may grow — they support `extends` and declaration merging. |
| 18 | - **Type aliases** for unions, intersections, mapped types, and complex compositions. |
| 19 | - Simple rule: `interface` for plain objects, `type` for everything else. |
| 20 | |
| 21 | ```typescript |
| 22 | // Interface: object shape, extensible |
| 23 | interface User { |
| 24 | id: string; |
| 25 | name: string; |
| 26 | } |
| 27 | |
| 28 | interface Employee extends User { |
| 29 | company: string; |
| 30 | } |
| 31 | |
| 32 | // Type: union, intersection, computed |
| 33 | type Result = Success | Failure; |
| 34 | type UserProfile = User & { bio: string }; |
| 35 | type Nullable<T> = { [K in keyof T]: T[K] | null }; |
| 36 | ``` |
| 37 | |
| 38 | ## Unions and Literal Types |
| 39 | |
| 40 | - **Prefer literal unions over enums** — zero runtime cost, better tree-shaking, full autocomplete. |
| 41 | - Use enums only when you need a runtime object (iteration, reverse lookup). |
| 42 | |
| 43 | ```typescript |
| 44 | // Prefer this |
| 45 | type HttpMethod = "GET" | "POST" | "PUT" | "DELETE"; |
| 46 | type Direction = "north" | "south" | "east" | "west"; |
| 47 | |
| 48 | // Over this (emits runtime JS) |
| 49 | enum HttpMethod { GET, POST, PUT, DELETE } |
| 50 | ``` |
| 51 | |
| 52 | ## Discriminated Unions |
| 53 | |
| 54 | Add a `type` (or `kind`) literal field to each variant. Always handle exhaustiveness with `assertNever`. |
| 55 | |
| 56 | ```typescript |
| 57 | interface Circle { type: "circle"; radius: number } |
| 58 | interface Square { type: "square"; side: number } |
| 59 | interface Triangle { type: "triangle"; base: number; height: number } |
| 60 | |
| 61 | type Shape = Circle | Square | Triangle; |
| 62 | |
| 63 | function assertNever(x: never): never { |
| 64 | throw new Error(`Unexpected value: ${x}`); |
| 65 | } |
| 66 | |
| 67 | function area(shape: Shape): number { |
| 68 | switch (shape.type) { |
| 69 | case "circle": return Math.PI * shape.radius ** 2; |
| 70 | case "square": return shape.side ** 2; |
| 71 | case "triangle": return (shape.base * shape.height) / 2; |
| 72 | default: return assertNever(shape); |
| 73 | } |
| 74 | } |
| 75 | ``` |
| 76 | |
| 77 | ## Type Narrowing |
| 78 | |
| 79 | Always narrow before accessing type-specific properties. |
| 80 | |
| 81 | - `typeof` for primitives: `typeof x === "string"` |
| 82 | - `in` for object shapes: `"swim" in pet` |
| 83 | - Custom type guards for reusable checks: `function isBook(item): item is Book` |
| 84 | |
| 85 | ```typescript |
| 86 | function format(input: string | number): string { |
| 87 | if (typeof input === "string") return input.toUpperCase(); |
| 88 | return input.toFixed(2); |
| 89 | } |
| 90 | |
| 91 | // Custom type guard |
| 92 | function isError(result: Result): result is ErrorResult { |
| 93 | return result.success === false; |
| 94 | } |
| 95 | ``` |
| 96 | |
| 97 | ## Generics |
| 98 | |
| 99 | - Constrain with `extends` — never assume properties exist on unconstrained `T`. |
| 100 | - Use defaults (`T = unknown`) when callers often use a single type. |
| 101 | - Keep generics to one or two parameters; more suggests the function is too broad. |
| 102 | |
| 103 | ```typescript |
| 104 | // Constrained generic |
| 105 | function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] { |
| 106 | return obj[key]; |
| 107 | } |
| 108 | |
| 109 | // With default |
| 110 | type ApiResponse<T = unknown> = { data: T; status: number }; |
| 111 | ``` |
| 112 | |
| 113 | ## Mapped & Template Literal Types |
| 114 | |
| 115 | Use mapped types to derive variants from a base — never duplicate type definitions. |
| 116 | |
| 117 | ```typescript |
| 118 | // All fields optional (equivalent to built-in Partial<T>) |
| 119 | type Optional<T> = { [K in keyof T]?: T[K] }; |
| 120 | |
| 121 | // All fields nullable |
| 122 | type Nullable<T> = { [K in keyof T]: T[K] | null }; |
| 123 | |
| 124 | // Key remapping with template literals |
| 125 | type Getters<T> = { |
| 126 | [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K]; |
| 127 | }; |
| 128 | ``` |
| 129 | |
| 130 | ## Intersection Types |
| 131 | |
| 132 | - Use `&` to compose smaller interfaces into richer types. |
| 133 | - If two intersected types define the same key with incompatible types, the result collapses to `never` — check for this. |
| 134 | |
| 135 | ```typescript |
| 136 | type Timestamped = { createdAt: Date; updatedAt: Date }; |
| 137 | type UserRecord = User & Timestamped; |
| 138 | ``` |
| 139 | |
| 140 | ## Imports |
| 141 | |
| 142 | ```typescript |
| 143 | // Type-only imports (required by verbatimModuleSyntax) |
| 144 | import type { FastifyInstance } from "fastify"; |
| 145 | |
| 146 | // Mixed imports: separate values and types |
| 147 | import { z } from "zod/v4"; |
| 148 | import type { ZodType } from "zod/v4"; |
| 149 | |
| 150 | // ioredis: always named import |
| 151 | import { Redis } from "ioredis"; |
| 152 | ``` |
| 153 | |
| 154 | ## Error Handling |
| 155 | |
| 156 | - Handle errors at the beginning of functio |