Table of contents
Open Table of contents
Overview
This post covers the foundational TypeScript concepts that appear throughout the kata exercises. It’s designed as a reference you can revisit as you work through data structure, design pattern, and algorithm katas.
The Type System
TypeScript uses a structural type system — types are compatible if their shapes match, regardless of explicit declarations.
interface Point {
x: number;
y: number;
}
// No explicit `implements Point`, but this works
const p = { x: 10, y: 20 };
const point: Point = p; // OK
Literal Types
Narrow types to specific values:
type Direction = "north" | "south" | "east" | "west";
type HttpStatus = 200 | 404 | 500;
Type Assertions vs Type Guards
Assertions (as) override the compiler. Guards (is, typeof, instanceof, in) refine types safely.
// Assertion -- you promise the compiler
const input = getInput() as string;
// Guard -- the compiler verifies
function isString(value: unknown): value is string {
return typeof value === "string";
}
Generics
Generics enable reusable, type-safe abstractions — critical for data structure katas.
class Stack<T> {
private items: T[] = [];
push(item: T): void {
this.items.push(item);
}
pop(): T | undefined {
return this.items.pop();
}
}
const numStack = new Stack<number>();
numStack.push(42);
Constraints
Restrict what types a generic can accept:
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
Generic Defaults
interface ApiResponse<T = unknown> {
data: T;
status: number;
}
Utility Types
Built-in types for common transformations:
| Type | Description | Example |
|---|---|---|
Partial<T> | All properties optional | Config overrides |
Required<T> | All properties required | Validation |
Readonly<T> | All properties readonly | Immutable data |
Pick<T, K> | Subset of properties | API responses |
Omit<T, K> | Exclude properties | Form data without ID |
Record<K, V> | Key-value map | Record<string, number> |
ReturnType<F> | Return type of function | Infer from existing code |
interface User {
id: number;
name: string;
email: string;
role: "admin" | "user";
}
type CreateUser = Omit<User, "id">;
type UserUpdate = Partial<Pick<User, "name" | "email">>;
Discriminated Unions
Combine union types with a shared literal property for exhaustive type narrowing:
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;
}
}
Exhaustiveness Checking
Use never to ensure all variants are handled:
function assertNever(x: never): never {
throw new Error(`Unexpected value: ${x}`);
}
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:
return assertNever(shape); // compile error if a variant is missed
}
}
Mapped Types
Transform existing types programmatically:
type Flags<T> = {
[K in keyof T]: boolean;
};
interface Features {
darkMode: string;
notifications: string;
}
type FeatureFlags = Flags<Features>;
// { darkMode: boolean; notifications: boolean }
Key Remapping
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
Conditional Types
Types that depend on conditions:
type IsArray<T> = T extends any[] ? true : false;
type A = IsArray<string[]>; // true
type B = IsArray<number>; // false
infer Keyword
Extract types from within other types:
type ElementType<T> = T extends (infer E)[] ? E : never;
type A = ElementType<string[]>; // string
type B = ElementType<number>; // never
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
type A = UnwrapPromise<Promise<string>>; // string
type B = UnwrapPromise<number>; // number
Template Literal Types
Build types from string patterns:
type EventName = `on${Capitalize<"click" | "focus" | "blur">}`;
// "onClick" | "onFocus" | "onBlur"
type CssProperty = `${string}-${string}`;
Declaration Merging
Interfaces with the same name merge automatically:
interface Window {
myCustomProperty: string;
}
// Now window.myCustomProperty is typed
Module Patterns
Barrel Exports
// utils/index.ts
export { slugify } from "./slugify";
export { formatDate } from "./date";
export type { PostFilter } from "./types";
Type-Only Imports
import type { User } from "./models";
Key Takeaways
- Structural typing — shape matters, not names
- Generics — the backbone of reusable data structures
- Discriminated unions — safe, exhaustive pattern matching
- Utility types — avoid rewriting common transformations
- Mapped + conditional types — build complex types from simple ones
Related Katas
- Kata: Linked List — generics for type-safe nodes
- Kata: Binary Search Tree — generic comparisons
- Kata: Factory Pattern — discriminated unions
- Kata: Strategy Pattern — interfaces and generics
- Kata: Hash Map — generics and utility types