typescript-patterns
Advanced types, strict mode, type safety
Installation
Follow the repository instructions for this skill and your agent. Review the files and dependencies before adding it to your workflow.
View source ↗GuidesSkill instructions
TypeScript Development Patterns
Advanced TypeScript patterns for type-safe code.
Strict Mode
Enable all strict checks in tsconfig.json:
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true
}
}
Basic Types
// Primitives
const name: string = "John";
const age: number = 30;
const isActive: boolean = true;
// Arrays
const numbers: number[] = [1, 2, 3];
const names: Array<string> = ["Alice", "Bob"];
// Tuples
const tuple: [string, number] = ["Alice", 30];
// Objects
const user: { name: string; age: number } = {
name: "Alice",
age: 30
};
Advanced Types
Union Types
type Status = "pending" | "success" | "error";
type ID = string | number;
function handleStatus(status: Status) {
// TypeScript knows status can only be those 3 values
}
Intersection Types
type Person = { name: string };
type Employee = { employeeId: number };
type Worker = Person & Employee;
const worker: Worker = {
name: "Alice",
employeeId: 123
};
Type Guards
function isString(value: unknown): value is string {
return typeof value === "string";
}
function process(value: string | number) {
if (isString(value)) {
// TypeScript knows value is string here
return value.toUpperCase();
}
// TypeScript knows value is number here
return value.toFixed(2);
}
Discriminated Unions
type Success = { status: "success"; data: string };
type Error = { status: "error"; error: string };
type Loading = { status: "loading" };
type State = Success | Error | Loading;
function handleState(state: State) {
switch (state.status) {
case "success":
return state.data; // TypeScript knows data exists
case "error":
return state.error; // TypeScript knows error exists
case "loading":
return "Loading...";
}
}
Generics
Basic Generics
function identity<T>(value: T): T {
return value;
}
const num = identity(42); // number
const str = identity("hello"); // string
Generic Constraints
function getLength<T extends { length: number }>(item: T): number {
return item.length;
}
getLength("hello"); // OK
getLength([1, 2, 3]); // OK
getLength(42); // Error: number has no length
Generic Interfaces
interface APIResponse<T> {
data: T;
status: number;
error?: string;
}
const userResponse: APIResponse<User> = {
data: { id: 1, name: "Alice" },
status: 200
};
Utility Types
Partial
interface User {
id: number;
name: string;
email: string;
}
// All properties optional
type PartialUser = Partial<User>;
function updateUser(id: number, updates: Partial<User>) {
// Can update any subset of User properties
}
Pick
// Select subset of properties
type UserPreview = Pick<User, "id" | "name">;
Omit
// Exclude properties
type UserWithoutEmail = Omit<User, "email">;
Required
interface Config {
host?: string;
port?: number;
}
// All properties required
type RequiredConfig = Required<Config>;
Record
type UserRoles = Record<string, string[]>;
const roles: UserRoles = {
admin: ["read", "write", "delete"],
user: ["read"]
};
ReturnType
function getUser() {
return { id: 1, name: "Alice" };
}
type User = ReturnType<typeof getUser>; // { id: number; name: string }
Mapped Types
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
type Nullable<T> = {
[P in keyof T]: T[P] | null;
};
interface User {
id: number;
name: string;
}
type ReadonlyUser = Readonly<User>;
type NullableUser = Nullable<User>;
Conditional Types
type IsString<T> = T extends string ? true : false;
type A = IsString<string>; // true
type B = IsString<number>; // false
// More complex
type Flatten<T> = T extends Array<infer U> ? U : T;
type Num = Flatten<number[]>; // number
type Str = Flatten<string>; // string
Template Literal Types
type HTTPMethod = "GET" | "POST" | "PUT" | "DELETE";
type Endpoint = `/api/${string}`;
type APIRoute = `${HTTPMethod} ${Endpoint}`;
const route: APIRoute = "GET /api/users"; // OK
const invalid: APIRoute = "PATCH /api/users"; // Error
Type Inference
// Let TypeScript infer when possible
const user = {
id: 1,
name: "Alice"
}; // Type is inferred
// Explicit when needed
const users: User[] = [];
// Generic inference
function map<T, U>(items: T[], fn: (item: T) => U): U[] {
return items.map(fn);
}
const lengths = map(["a", "ab", "abc"], s => s.length); // number[]
Null Safety
// Use strict null checks
let value: string | null = null;
// Optional chaining
const length = value?.length; // number | undefined
// Nullish coalescing
const name = value ?? "default";
// Non-null assertion (use sparingly!)
const definitelyExists = value!.length;
Index Signatures
interface StringMap {
[key: string]: string;
}
const config: StringMap = {
host: "localhost",
port: "3000"
};
// With noUncheckedIndexedAccess
const port = config["port"]; // string | undefined
Function Overloads
function process(value: string): string;
function process(value: number): number;
function process(value: string | number): string | number {
if (typeof value === "string") {
return value.toUpperCase();
}
return value * 2;
}
const str = process("hello"); // string
const num = process(42); // number
Type Assertions
// As syntax (preferred)
const canvas = document.getElementById("canvas") as HTMLCanvasElement;
// Angle bracket (avoid in TSX)
const canvas = <HTMLCanvasElement>document.getElementById("canvas");
// Double assertion (last resort)
const value = (unknown as any) as MyType;
Enums
// Numeric enum
enum Status {
Pending,
Success,
Error
}
// String enum (preferred)
enum Color {
Red = "RED",
Green = "GREEN",
Blue = "BLUE"
}
// Const enum (inlined at compile time)
const enum Direction {
Up,
Down,
Left,
Right
}
Type Narrowing
function example(value: string | number | null) {
// typeof narrowing
if (typeof value === "string") {
return value.toUpperCase();
}
// Truthiness narrowing
if (value) {
return value.toFixed(2);
}
// Equality narrowing
if (value === null) {
return "null";
}
}
Best Practices
✅ Do:
- Enable strict mode
- Use
unknowninstead ofany - Leverage type inference
- Use discriminated unions for complex state
- Prefer interfaces for objects
- Use type aliases for unions/intersections
- Add return types to public APIs
❌ Don't:
- Use
any(useunknowninstead) - Overuse type assertions
- Ignore TypeScript errors
- Use
as anyto bypass errors - Use
!(non-null assertion) without understanding - Define types for everything (let inference work)
Common Patterns
Result Type
type Result<T, E = Error> =
| { ok: true; value: T }
| { ok: false; error: E };
function divide(a: number, b: number): Result<number> {
if (b === 0) {
return { ok: false, error: new Error("Division by zero") };
}
return { ok: true, value: a / b };
}
Builder Pattern
class QueryBuilder {
private query = "";
where(condition: string): this {
this.query += ` WHERE ${condition}`;
return this;
}
orderBy(field: string): this {
this.query += ` ORDER BY ${field}`;
return this;
}
build(): string {
return this.query;
}
}
Type-safe Event Emitter
type Events = {
userCreated: { id: number; name: string };
userDeleted: { id: number };
};
class TypedEmitter<T extends Record<string, any>> {
on<K extends keyof T>(event: K, handler: (data: T[K]) => void) {
// Implementation
}
emit<K extends keyof T>(event: K, data: T[K]) {
// Implementation
}
}
const emitter = new TypedEmitter<Events>();
emitter.on("userCreated", data => {
// data is typed as { id: number; name: string }
});
Anti-Patterns
❌ Using any everywhere
❌ Ignoring compiler errors
❌ Over-using type assertions
❌ Not enabling strict mode
❌ Defining types for simple inferred values
Examples
Read the usage examples in the source repository.
View source ↗Source history
- Indexed on
- 2026-01-20
- Index updated
- 2026-01-26
- Branch
- main
Index and sync dates describe this directory. View the source repository for code history.
GitHub ↗chat Comments (0)
Sign in to join the discussion and leave a comment.
No comments yet. Be the first to share your thoughts!