TypeScript Mastery
Overview
TypeScript strict mode is enforced in Buzz Stack. This skill covers advanced patterns for maximum type safety, reducing runtime errors and improving code clarity through the type system.
Why it matters:
- •
anytype leaks defeat the purpose of TypeScript - •Generics enable reusable, type-safe code
- •Discriminated unions catch invalid states at compile time
- •Type-level patterns prevent entire classes of bugs
Core Concepts
1. Strict Mode & Implications
typescript
// tsconfig.json must have:
{
"compilerOptions": {
"strict": true, // ALL strict checks enabled
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"strictBindCallApply": true,
"strictPropertyInitialization": true,
"noImplicitThis": true,
"alwaysStrict": true
}
}
Effect:
- •No implicit
any - •No accessing undefined properties
- •No loose function typing
- •Properties must be initialized
2. Generic Constraints (Type-Safety for Reusability)
typescript
// ❌ WRONG: Generic without constraints
function getValue<T>(obj: T, key: string): unknown {
return obj[key]; // TS can't guarantee key exists
}
// ✅ CORRECT: Constrain generic to key type
function getValue<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key]; // Now type-safe!
}
// Usage
interface User {
id: string;
name: string;
}
const user: User = { id: "1", name: "Alice" };
const name = getValue(user, "name"); // Type: string ✓
const age = getValue(user, "age"); // ❌ Compile error - age doesn't exist
3. Discriminated Unions (Type-Safe State)
typescript
// ❌ WRONG: Multiple optional fields (invalid states possible)
interface User {
status?: "loading" | "success" | "error";
data?: UserData;
error?: Error;
}
// Invalid: status='loading' but data=undefined AND error=undefined
// ✅ CORRECT: Discriminated union (only valid states)
type UserState =
| { status: "idle" }
| { status: "loading" }
| { status: "success"; data: UserData }
| { status: "error"; error: Error };
// Usage
function handleUserState(state: UserState) {
switch (state.status) {
case "idle":
// Can't access .data or .error here
break;
case "success":
console.log(state.data); // ✓ Guaranteed to exist
break;
case "error":
console.log(state.error); // ✓ Guaranteed to exist
break;
}
}
4. Conditional Types (Type-Level Programming)
typescript
// Extract the return type from a function type
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
// Usage
type MyFunc = (x: number) => string;
type MyReturn = ReturnType<MyFunc>; // string
// Real-world: Unwrap Promise types
type Awaited<T> = T extends Promise<infer U> ? U : T;
type ApiResponse = Promise<{ data: string }>;
type Unwrapped = Awaited<ApiResponse>; // { data: string }
5. Mapped Types (Generate Types Automatically)
typescript
// Create getters for all properties
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
// Usage
interface User {
id: string;
name: string;
email: string;
}
type UserGetters = Getters<User>;
// Result:
// {
// getId: () => string;
// getName: () => string;
// getEmail: () => string;
// }
// Real implementation
const userGetters: UserGetters = {
getId: () => "1",
getName: () => "Alice",
getEmail: () => "alice@example.com",
};
6. Type Guards (Assertion Functions)
typescript
// ❌ WRONG: No type narrowing
function processValue(val: string | number) {
if (typeof val === "string") {
// TS still doesn't know val is string here
console.log(val.length);
}
}
// ✅ CORRECT: Type guard narrows type
function isString(val: unknown): val is string {
return typeof val === "string";
}
function processValue(val: string | number) {
if (isString(val)) {
// Now TS knows val is string
console.log(val.length);
}
}
// For custom types
function isUser(obj: unknown): obj is User {
return (
typeof obj === "object" &&
obj !== null &&
"id" in obj &&
"name" in obj &&
typeof obj.id === "string" &&
typeof obj.name === "string"
);
}
Deep Patterns
Pattern 1: Result Type (Error Handling)
typescript
// Explicit error handling at type level
type Result<T, E> = { ok: true; value: T } | { ok: false; error: E };
// Usage
async function fetchUser(id: string): Promise<Result<User, HttpError>> {
try {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) {
return { ok: false, error: new HttpError(response.status) };
}
const data = await response.json();
return { ok: true, value: data };
} catch (error) {
return { ok: false, error };
}
}
// Forced handling
const result = await fetchUser("123");
if (result.ok) {
console.log(result.value.name); // ✓ Guaranteed User
} else {
console.log(result.error.message); // ✓ Guaranteed Error
}
Pattern 2: Builder Pattern with Types
typescript
class QueryBuilder<T = never> {
private filters: string[] = [];
private limits: number | undefined;
where<U extends T>(
field: Extract<keyof T, string>,
value: T[U],
): QueryBuilder<T> {
this.filters.push(`${field}=${value}`);
return this;
}
limit(n: number): QueryBuilder<T> {
this.limits = n;
return this;
}
build(): string {
return `SELECT * WHERE ${this.filters.join(" AND ")} LIMIT ${this.limits}`;
}
}
// Usage with type checking
interface User {
id: string;
name: string;
email: string;
}
const query = new QueryBuilder<User>()
.where("name", "Alice") // ✓ 'name' exists on User
.where("age", 30) // ❌ 'age' doesn't exist on User
.limit(10)
.build();
Pattern 3: Generic Service Layer (Type-Safe Orchestration)
typescript
// Service that works with ANY data type
abstract class Service<T> {
protected cache = new Map<string, T>();
async get(id: string): Promise<T | null> {
const cached = this.cache.get(id);
if (cached) return cached;
const item = await this.fetch(id);
if (item) {
this.cache.set(id, item);
}
return item;
}
protected abstract fetch(id: string): Promise<T | null>;
}
// Concrete implementation
interface User {
id: string;
name: string;
}
class UserService extends Service<User> {
protected async fetch(id: string): Promise<User | null> {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) return null;
return response.json();
}
}
// Strongly typed
const userService = new UserService();
const user = await userService.get("123"); // Type: User | null ✓
Pattern 4: Utility Types (Use Existing, Don't Reinvent)
typescript
// Pick - select specific properties
type UserInfo = Pick<User, "id" | "name">;
// Omit - exclude specific properties
type UserWithoutEmail = Omit<User, "email">;
// Record - create object with known keys
type Permissions = Record<"read" | "write" | "delete", boolean>;
// Result: { read: boolean; write: boolean; delete: boolean; }
// Partial - make all properties optional
type PartialUser = Partial<User>;
// Required - make all properties required
type FullUser = Required<Partial<User>>;
// ReadOnly - prevent mutations
type ReadonlyUser = Readonly<User>;
// Extract - get matching types
type StringKeys = Extract<keyof User, string>;
// Exclude - remove matching types
type NonStringKeys = Exclude<keyof User, string>;
Anti-Patterns to Avoid
typescript
// ❌ Wrong: Using any
function processData(data: any) {
return data.toUpperCase(); // No error, runs quietly
}
// ✅ Correct: Specific types
function processData(data: string) {
return data.toUpperCase();
}
// ❌ Wrong: Multiple optional properties
interface Response {
data?: unknown;
error?: unknown;
}
// ✅ Correct: Discriminated union
type Response = { data: unknown } | { error: unknown };
// ❌ Wrong: Generic without constraints
function getValue<T>(obj: T, key: string) {
return obj[key]; // Unsafe
}
// ✅ Correct: Constrain the generic
function getValue<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
Compilation Strategy
- •Enable strict mode (non-negotiable for type safety)
- •Use
noUnusedLocalsandnoUnusedParameters(catch dead code) - •Use
exactOptionalPropertyTypes(undefined vs. optional) - •Use
noImplicitOverride(catch override mistakes) - •Use
noUncheckedIndexedAccess(access safety)
typescript
// tsconfig.json - COMPLETE STRICT SETUP
{
"compilerOptions": {
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"noImplicitOverride": true,
"skipLibCheck": false // Don't skip checking node_modules
}
}
Real-World Application in Buzz Stack
- •React props: Discriminated component states
- •Service layer: Generics for type-safe caching
- •API contracts: Result<T, E> for error handling
- •Form handling: Type-safe form state with discriminators
- •Hook composition: Generic hooks with constraints
- •Utility functions: Extract common patterns with mapped types
Key Questions When Writing Types
- •Is this type accurate? (Does it match reality?)
- •Could invalid state be created? (Discriminate unions)
- •Is the generic properly constrained? (Use
extends) - •Can I make this reusable? (Generics, mapped types)
- •Am I using
any? (Stop. Find another way.)
Resources
- •TypeScript Handbook
- •Conditional Types Deep Dive
- •Type Challenges
- •Project: Stack Overflow, GitHub issues (for real patterns)