skills/typescript-patterns/SKILL.md
TypeScript patterns, advanced typing, and type safety. Use when user asks to "write TypeScript generics", "create utility types", "fix TypeScript errors", "type a complex object", "use conditional types", "create type guards", "improve TypeScript types", "design type-safe APIs", "use mapped types", "create branded types", "infer types", "type narrowing", "type variance", "overload signatures", "type assertion", or mentions TypeScript patterns, advanced typing, generic constraints, discriminated unions, template literal types, or type inference.
npx skillsauth add 1mangesh1/dev-skills-collection typescript-patternsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Production-grade TypeScript patterns for building type-safe, maintainable, and scalable applications. This skill covers generics, utility types, conditional types, discriminated unions, branded types, and more.
// Constrain T to objects that have a .length property
function getLength<T extends { length: number }>(item: T): number {
return item.length;
}
getLength("hello"); // OK - string has .length
getLength([1, 2, 3]); // OK - array has .length
// getLength(42); // Error - number has no .length
interface HasId {
id: string;
}
interface HasTimestamp {
createdAt: Date;
updatedAt: Date;
}
// T must satisfy both interfaces
function updateEntity<T extends HasId & HasTimestamp>(
entity: T,
updates: Partial<Omit<T, "id" | "createdAt">>
): T {
return { ...entity, ...updates, updatedAt: new Date() };
}
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { name: "Alice", age: 30, email: "[email protected]" };
const name = getProperty(user, "name"); // type: string
const age = getProperty(user, "age"); // type: number
// getProperty(user, "phone"); // Error: "phone" not in keyof user
interface Constructor<T> {
new (...args: any[]): T;
}
function createInstance<T>(ctor: Constructor<T>, ...args: any[]): T {
return new ctor(...args);
}
class UserService {
constructor(public apiUrl: string) {}
}
const service = createInstance(UserService, "https://api.example.com");
// service is typed as UserService
interface User {
id: string;
name: string;
email: string;
avatar?: string;
role: "admin" | "user" | "moderator";
preferences: {
theme: "light" | "dark";
notifications: boolean;
};
}
// Pick only the fields you need
type UserSummary = Pick<User, "id" | "name" | "avatar">;
// Omit sensitive fields
type PublicUser = Omit<User, "email" | "preferences">;
// Make all fields optional (useful for update payloads)
type UserUpdate = Partial<Omit<User, "id">>;
// Make optional fields required
type CompleteUser = Required<User>;
// avatar is now required
// Record: create an object type with known keys
type UserRole = "admin" | "user" | "moderator";
type RolePermissions = Record<UserRole, string[]>;
const permissions: RolePermissions = {
admin: ["read", "write", "delete", "manage-users"],
user: ["read", "write"],
moderator: ["read", "write", "delete"],
};
// Extract: pull types from a union that match
type NumericEvent = Extract<"click" | "scroll" | "keypress" | 42, string>;
// Result: "click" | "scroll" | "keypress"
// Exclude: remove types from a union
type NonAdminRole = Exclude<UserRole, "admin">;
// Result: "user" | "moderator"
function fetchUser(id: string, options?: { includeProfile: boolean }) {
return { id, name: "Alice", email: "[email protected]" };
}
type FetchUserReturn = ReturnType<typeof fetchUser>;
// { id: string; name: string; email: string }
type FetchUserParams = Parameters<typeof fetchUser>;
// [id: string, options?: { includeProfile: boolean }]
class ApiClient {
baseUrl: string;
constructor(baseUrl: string) {
this.baseUrl = baseUrl;
}
}
type ApiClientInstance = InstanceType<typeof ApiClient>;
// ApiClient
infer Keywordtype IsString<T> = T extends string ? true : false;
type A = IsString<"hello">; // true
type B = IsString<42>; // false
infer// Extract the return type of a promise
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
type Result = UnwrapPromise<Promise<string>>; // string
type Plain = UnwrapPromise<number>; // number
// Extract element type from an array
type ElementOf<T> = T extends (infer E)[] ? E : never;
type Item = ElementOf<string[]>; // string
type Nested = ElementOf<number[]>; // number
// Extract function arguments
type FirstArg<T> = T extends (first: infer F, ...rest: any[]) => any ? F : never;
type Arg = FirstArg<(name: string, age: number) => void>; // string
// Conditional types distribute over unions automatically
type ToArray<T> = T extends any ? T[] : never;
type Result = ToArray<string | number>;
// string[] | number[] (NOT (string | number)[])
// Prevent distribution with wrapping in tuple
type ToArrayNonDist<T> = [T] extends [any] ? T[] : never;
type Result2 = ToArrayNonDist<string | number>;
// (string | number)[]
// Deeply unwrap nested promises
type DeepUnwrap<T> = T extends Promise<infer U> ? DeepUnwrap<U> : T;
type Deep = DeepUnwrap<Promise<Promise<Promise<string>>>>; // string
// Deep partial - make all nested properties optional
type DeepPartial<T> = T extends object
? { [K in keyof T]?: DeepPartial<T[K]> }
: T;
interface Config {
server: {
host: string;
port: number;
ssl: {
enabled: boolean;
cert: string;
};
};
}
type PartialConfig = DeepPartial<Config>;
// All nested fields are now optional
// Make all properties readonly
type Immutable<T> = {
readonly [K in keyof T]: T[K] extends object ? Immutable<T[K]> : T[K];
};
// Make all properties nullable
type Nullable<T> = {
[K in keyof T]: T[K] | null;
};
// Add a prefix to all keys
type Prefixed<T, P extends string> = {
[K in keyof T as `${P}${Capitalize<string & K>}`]: T[K];
};
interface FormData {
name: string;
email: string;
}
type OnChangeHandlers = Prefixed<FormData, "onChange">;
// { onChangeName: string; onChangeEmail: string }
type HTTPMethod = "GET" | "POST" | "PUT" | "DELETE" | "PATCH";
type APIVersion = "v1" | "v2";
type Resource = "users" | "posts" | "comments";
// Generate all route combinations
type APIRoute = `/${APIVersion}/${Resource}`;
// "/v1/users" | "/v1/posts" | "/v1/comments" | "/v2/users" | ...
// Event handler types
type DOMEvent = "click" | "focus" | "blur" | "input";
type EventHandler = `on${Capitalize<DOMEvent>}`;
// "onClick" | "onFocus" | "onBlur" | "onInput"
// CSS unit types
type CSSUnit = "px" | "rem" | "em" | "vh" | "vw" | "%";
type CSSValue = `${number}${CSSUnit}`;
const width: CSSValue = "100px"; // OK
const height: CSSValue = "50vh"; // OK
// const bad: CSSValue = "auto"; // Error
as// Create getter functions for each property
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
interface Person {
name: string;
age: number;
}
type PersonGetters = Getters<Person>;
// { getName: () => string; getAge: () => number }
// Filter keys by value type
type StringKeysOnly<T> = {
[K in keyof T as T[K] extends string ? K : never]: T[K];
};
interface Mixed {
name: string;
age: number;
email: string;
active: boolean;
}
type OnlyStrings = StringKeysOnly<Mixed>;
// { name: string; email: string }
interface LoadingState {
status: "loading";
}
interface SuccessState<T> {
status: "success";
data: T;
}
interface ErrorState {
status: "error";
error: {
message: string;
code: number;
};
}
type RequestState<T> = LoadingState | SuccessState<T> | ErrorState;
function renderState<T>(state: RequestState<T>): string {
switch (state.status) {
case "loading":
return "Loading...";
case "success":
return `Data: ${JSON.stringify(state.data)}`;
case "error":
return `Error ${state.error.code}: ${state.error.message}`;
}
}
never// This function ensures all union variants are handled at compile time
function assertNever(value: never): never {
throw new Error(`Unhandled discriminated union member: ${JSON.stringify(value)}`);
}
type Shape =
| { kind: "circle"; radius: number }
| { kind: "rectangle"; width: number; height: number }
| { kind: "triangle"; base: number; height: number };
function area(shape: Shape): number {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "rectangle":
return shape.width * shape.height;
case "triangle":
return (shape.base * shape.height) / 2;
default:
// If a new variant is added to Shape but not handled here,
// TypeScript will produce a compile error on this line.
return assertNever(shape);
}
}
type Action =
| { type: "ADD_TODO"; payload: { text: string } }
| { type: "TOGGLE_TODO"; payload: { id: number } }
| { type: "REMOVE_TODO"; payload: { id: number } }
| { type: "SET_FILTER"; payload: { filter: "all" | "active" | "completed" } };
interface TodoState {
todos: Array<{ id: number; text: string; completed: boolean }>;
filter: "all" | "active" | "completed";
}
function todoReducer(state: TodoState, action: Action): TodoState {
switch (action.type) {
case "ADD_TODO":
return {
...state,
todos: [
...state.todos,
{ id: Date.now(), text: action.payload.text, completed: false },
],
};
case "TOGGLE_TODO":
return {
...state,
todos: state.todos.map((t) =>
t.id === action.payload.id ? { ...t, completed: !t.completed } : t
),
};
case "REMOVE_TODO":
return {
...state,
todos: state.todos.filter((t) => t.id !== action.payload.id),
};
case "SET_FILTER":
return { ...state, filter: action.payload.filter };
default:
return assertNever(action as never);
}
}
interface Cat {
type: "cat";
purrs: boolean;
}
interface Dog {
type: "dog";
barks: boolean;
}
type Animal = Cat | Dog;
// Type predicate: `animal is Cat`
function isCat(animal: Animal): animal is Cat {
return animal.type === "cat";
}
function handleAnimal(animal: Animal) {
if (isCat(animal)) {
console.log(animal.purrs); // TypeScript knows this is Cat
} else {
console.log(animal.barks); // TypeScript knows this is Dog
}
}
function assertIsString(value: unknown): asserts value is string {
if (typeof value !== "string") {
throw new Error(`Expected string, got ${typeof value}`);
}
}
function processInput(input: unknown) {
assertIsString(input);
// After assertion, TypeScript knows input is string
console.log(input.toUpperCase());
}
in Operator Narrowinginterface APIResponse {
data: unknown;
}
interface APIError {
error: string;
statusCode: number;
}
function handleResponse(res: APIResponse | APIError) {
if ("error" in res) {
// TypeScript narrows to APIError
console.error(`Error ${res.statusCode}: ${res.error}`);
} else {
// TypeScript narrows to APIResponse
console.log(res.data);
}
}
satisfies// satisfies validates the type without widening it
type Color = "red" | "green" | "blue";
type ColorMap = Record<Color, string | [number, number, number]>;
const palette = {
red: [255, 0, 0],
green: "#00ff00",
blue: [0, 0, 255],
} satisfies ColorMap;
// palette.red is inferred as [number, number, number], not string | [number, number, number]
const redChannel = palette.red[0]; // OK - TypeScript knows it is a tuple
// Brand type utility
type Brand<T, B extends string> = T & { readonly __brand: B };
type UserId = Brand<string, "UserId">;
type OrderId = Brand<string, "OrderId">;
type Email = Brand<string, "Email">;
// Constructor functions with validation
function createUserId(id: string): UserId {
if (!id.match(/^usr_[a-zA-Z0-9]{12}$/)) {
throw new Error("Invalid user ID format");
}
return id as UserId;
}
function createEmail(email: string): Email {
if (!email.includes("@")) {
throw new Error("Invalid email format");
}
return email as Email;
}
function sendEmail(to: Email, subject: string): void {
// ...
}
function getUser(id: UserId): void {
// ...
}
const userId = createUserId("usr_abc123def456");
const email = createEmail("[email protected]");
sendEmail(email, "Welcome!"); // OK
// sendEmail(userId, "Welcome!"); // Error: UserId is not assignable to Email
// getUser(email); // Error: Email is not assignable to UserId
type Celsius = Brand<number, "Celsius">;
type Fahrenheit = Brand<number, "Fahrenheit">;
type Kilometers = Brand<number, "Kilometers">;
type Miles = Brand<number, "Miles">;
function celsiusToFahrenheit(c: Celsius): Fahrenheit {
return ((c * 9) / 5 + 32) as Fahrenheit;
}
function milesToKilometers(m: Miles): Kilometers {
return (m * 1.60934) as Kilometers;
}
const temp = 100 as Celsius;
const converted = celsiusToFahrenheit(temp); // Fahrenheit
// celsiusToFahrenheit(100 as Fahrenheit); // Error: type mismatch
interface QueryConfig {
table: string;
columns: string[];
where: string[];
orderBy: string | null;
limit: number | null;
}
type RequiredFields = "table" | "columns";
class QueryBuilder<T extends Partial<QueryConfig> = {}> {
private config: Partial<QueryConfig> = {};
from<Table extends string>(
table: Table
): QueryBuilder<T & { table: Table }> {
this.config.table = table;
return this as any;
}
select(
...columns: string[]
): QueryBuilder<T & { columns: string[] }> {
this.config.columns = columns;
return this as any;
}
where(condition: string): QueryBuilder<T & { where: string[] }> {
this.config.where = [...(this.config.where ?? []), condition];
return this as any;
}
orderBy(column: string): this {
this.config.orderBy = column;
return this;
}
limit(count: number): this {
this.config.limit = count;
return this;
}
// Only available when required fields are set
build(
this: QueryBuilder<Pick<QueryConfig, RequiredFields>>
): QueryConfig {
return {
table: this.config.table!,
columns: this.config.columns!,
where: this.config.where ?? [],
orderBy: this.config.orderBy ?? null,
limit: this.config.limit ?? null,
};
}
}
// Usage
const query = new QueryBuilder()
.from("users")
.select("id", "name", "email")
.where("active = true")
.orderBy("name")
.limit(10)
.build(); // Only compiles because from() and select() were called
import { z } from "zod";
// Define schema
const UserSchema = z.object({
id: z.string().uuid(),
name: z.string().min(1).max(100),
email: z.string().email(),
age: z.number().int().min(0).max(150).optional(),
role: z.enum(["admin", "user", "moderator"]),
tags: z.array(z.string()).default([]),
metadata: z.record(z.string(), z.unknown()).optional(),
createdAt: z.coerce.date(),
});
// Infer the TypeScript type from the schema
type User = z.infer<typeof UserSchema>;
// {
// id: string;
// name: string;
// email: string;
// age?: number;
// role: "admin" | "user" | "moderator";
// tags: string[];
// metadata?: Record<string, unknown>;
// createdAt: Date;
// }
// Validate at runtime
function createUser(input: unknown): User {
return UserSchema.parse(input); // throws ZodError if invalid
}
// Safe parsing (no throw)
function tryCreateUser(input: unknown): User | null {
const result = UserSchema.safeParse(input);
if (result.success) {
return result.data;
}
console.error("Validation errors:", result.error.flatten());
return null;
}
const CreateUserRequest = UserSchema.omit({ id: true, createdAt: true });
const UpdateUserRequest = CreateUserRequest.partial();
const APIResponse = <T extends z.ZodType>(dataSchema: T) =>
z.object({
success: z.boolean(),
data: dataSchema,
meta: z
.object({
page: z.number(),
total: z.number(),
})
.optional(),
});
const UserListResponse = APIResponse(z.array(UserSchema));
type UserListResponse = z.infer<typeof UserListResponse>;
// Problem
const status: "active" | "inactive" = "active";
let mutable = status;
// mutable is inferred as string, not the literal type
// Fix: use const assertion or explicit type
let mutable: typeof status = status;
// OR
const statuses = ["active", "inactive"] as const;
type Status = (typeof statuses)[number]; // "active" | "inactive"
// Problem: passing an object literal with extra properties
interface Options {
timeout: number;
}
function configure(opts: Options) {}
// Fix: use a variable (excess property check only applies to literals)
const opts = { timeout: 5000, retries: 3 };
configure(opts); // OK - no excess property check on variables
// Problem
const config: Record<string, unknown> = {};
const value = config["key"]; // unknown
// Fix: use a type guard or assertion
function getString(obj: Record<string, unknown>, key: string): string {
const val = obj[key];
if (typeof val !== "string") {
throw new Error(`Expected string at key "${key}"`);
}
return val;
}
// Problem with union types
type Result = { ok: true; value: string } | { ok: false; error: string };
function handle(result: Result) {
// result.value // Error: property doesn't exist on { ok: false; ... }
// Fix: narrow the type first
if (result.ok) {
console.log(result.value); // OK after narrowing
} else {
console.log(result.error);
}
}
// Extend a third-party library's types
declare module "express" {
interface Request {
user?: {
id: string;
role: string;
};
requestId: string;
}
}
// Now express Request has your custom fields
import { Request } from "express";
function handler(req: Request) {
console.log(req.user?.id); // OK
console.log(req.requestId); // OK
}
// Add custom properties to globalThis
declare global {
interface Window {
__APP_CONFIG__: {
apiUrl: string;
environment: "development" | "staging" | "production";
};
}
// Add utility to Array prototype (declaration only)
interface Array<T> {
groupBy<K extends string>(fn: (item: T) => K): Record<K, T[]>;
}
}
export {}; // Required to make this a module
// Merge a namespace with a function (overloaded module pattern)
function currency(amount: number): string;
function currency(amount: number, code: string): string;
function currency(amount: number, code?: string): string {
return `${(code ?? "USD")} ${amount.toFixed(2)}`;
}
namespace currency {
export const codes = ["USD", "EUR", "GBP", "JPY"] as const;
export type Code = (typeof codes)[number];
export function convert(amount: number, from: Code, to: Code): number {
// conversion logic
return amount;
}
}
currency(42); // "USD 42.00"
currency.convert(100, "USD", "EUR"); // uses namespace
{
"compilerOptions": {
// Enable all strict checks
"strict": true,
// Individual flags (all enabled by "strict": true)
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"strictBindCallApply": true,
"strictPropertyInitialization": true,
"noImplicitThis": true,
"alwaysStrict": true,
// Additional strictness beyond "strict"
"noUncheckedIndexedAccess": true, // arr[0] is T | undefined
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"noImplicitOverride": true,
"exactOptionalPropertyTypes": true,
"noPropertyAccessFromIndexSignature": true,
// Module and target
"module": "NodeNext",
"moduleResolution": "NodeNext",
"target": "ES2022",
"lib": ["ES2022"],
// Output
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "**/*.test.ts"]
}
// With strictNullChecks enabled
function getUser(id: string): User | undefined {
return users.get(id);
}
// Option 1: Early return guard
function getUserName(id: string): string {
const user = getUser(id);
if (!user) {
throw new Error(`User ${id} not found`);
}
return user.name; // user is narrowed to User
}
// Option 2: Non-null assertion (use sparingly, only when you are certain)
function unsafeGetName(id: string): string {
return getUser(id)!.name;
}
// Option 3: Optional chaining with nullish coalescing
function safeGetName(id: string): string {
return getUser(id)?.name ?? "Anonymous";
}
// With noUncheckedIndexedAccess, array/record access returns T | undefined
const items: string[] = ["a", "b", "c"];
const first = items[0]; // string | undefined
// Guard before use
if (first !== undefined) {
console.log(first.toUpperCase()); // OK
}
// Or use a helper
function at<T>(arr: T[], index: number): T {
const item = arr[index];
if (item === undefined) {
throw new RangeError(`Index ${index} out of bounds`);
}
return item;
}
| Pattern | Use Case | |---|---| | Generics | Reusable functions/classes that work with multiple types | | Utility types | Transform existing types (pick fields, make optional, etc.) | | Conditional types | Types that depend on other types at compile time | | Mapped types | Transform all properties of a type systematically | | Template literals | Generate string literal union types | | Discriminated unions | Model states, events, actions with tagged variants | | Type guards | Runtime checks that narrow types for the compiler | | Branded types | Prevent mixing up primitives (UserId vs OrderId) | | Builder pattern | Enforce required steps at compile time | | Zod schemas | Runtime validation with automatic type inference | | Declaration merging | Extend third-party or global types | | Strict mode | Catch more bugs at compile time |
tools
Parallel execution with xargs, GNU parallel, and batch processing patterns. Use when user mentions "xargs", "parallel", "batch processing", "run in parallel", "parallel execution", "process list of files", "bulk operations", "concurrent commands", "map over files", or running commands on multiple inputs.
development
WebSocket implementation for real-time bidirectional communication. Use when user mentions "websocket", "ws://", "wss://", "real-time", "live updates", "chat application", "socket.io", "Server-Sent Events", "SSE", "push notifications", "live data", "streaming data", "bidirectional communication", "websocket server", "reconnection", or building real-time features.
tools
Frontend bundler configuration for Webpack and Vite. Use when user mentions "webpack", "vite", "bundler", "vite config", "webpack config", "code splitting", "tree shaking", "hot module replacement", "HMR", "build optimization", "bundle size", "chunk splitting", "loader", "plugin", "esbuild", "rollup", "dev server", or configuring JavaScript build tools.
tools
VS Code configuration, extensions, keybindings, and workspace optimization. Use when user mentions "vscode", "vs code", "vscode settings", "vscode extensions", "keybindings", "code editor", "workspace settings", "settings.json", "launch.json", "tasks.json", "vscode snippets", "devcontainer", "remote development", or customizing their VS Code setup.