- Authors

- Name
- Youngju Kim
- @fjvbn20031
TypeScript Complete Guide: Type System to Advanced Patterns
TypeScript adds a static type system on top of JavaScript. As of 2026, most large-scale projects — both frontend and backend — have adopted TypeScript as their standard. This guide covers everything from the basics to advanced patterns that you can apply directly in production code.
1. TypeScript Type System Fundamentals
1.1 Primitive Types
TypeScript's primitive types mirror JavaScript's but are declared explicitly.
// Basic primitive types
const name: string = 'Alice'
const age: number = 30
const isActive: boolean = true
const nothing: null = null
const undef: undefined = undefined
// ES6+ types
const sym: symbol = Symbol('unique')
const big: bigint = 9007199254740991n
// Type inference - TypeScript infers the type from the assigned value
const inferredName = 'Alice' // inferred as string
const inferredAge = 30 // inferred as number
1.2 Arrays and Tuples
// Two equivalent array type notations
const nums1: number[] = [1, 2, 3]
const nums2: Array<number> = [1, 2, 3]
// Read-only array
const readonlyNums: readonly number[] = [1, 2, 3]
// readonlyNums.push(4) // Error: push does not exist on readonly array
// Tuple - fixed length and type at each position
const pair: [string, number] = ['Alice', 30]
const triple: [string, number, boolean] = ['Bob', 25, true]
// Optional tuple element
const optional: [string, number?] = ['Charlie']
// Rest tuple
const rest: [string, ...number[]] = ['start', 1, 2, 3, 4]
1.3 Enums
// Numeric enum (default: starts from 0)
enum Direction {
Up, // 0
Down, // 1
Left, // 2
Right, // 3
}
// String enum
enum Color {
Red = 'RED',
Green = 'GREEN',
Blue = 'BLUE',
}
// const enum - inlined at compile time, reducing bundle size
const enum Status {
Active = 'ACTIVE',
Inactive = 'INACTIVE',
Pending = 'PENDING',
}
// When using const enum, the value is substituted directly
const currentStatus = Status.Active // compiled to: 'ACTIVE'
// When to choose regular enum vs const enum:
// - Need to iterate the enum object at runtime: use regular enum
// - Performance matters and iteration is not needed: use const enum
1.4 any vs unknown vs never vs void
These four special types are the most commonly confused in TypeScript.
// any - completely disables type checking (avoid when possible)
let anyVal: any = 'string'
anyVal = 42 // OK
anyVal = true // OK
anyVal.nonExistent() // possible runtime error, no compile error
// unknown - safer alternative to any
let unknownVal: unknown = 'string'
// unknownVal.toUpperCase() // Error: needs type narrowing first
if (typeof unknownVal === 'string') {
unknownVal.toUpperCase() // OK: after type guard
}
// never - type for values that never occur
function throwError(msg: string): never {
throw new Error(msg)
}
function infiniteLoop(): never {
while (true) {}
}
// never is useful for exhaustiveness checks
type Shape = 'circle' | 'square' | 'triangle'
function area(shape: Shape): number {
switch (shape) {
case 'circle':
return Math.PI * 1
case 'square':
return 1
case 'triangle':
return 0.5
default:
// shape must be never here - adding a new Shape causes a compile error
const _exhaustive: never = shape
throw new Error(`Unknown shape: ${_exhaustive}`)
}
}
// void - for functions with no return value
function logMessage(msg: string): void {
console.log(msg)
}
1.5 Type Alias vs Interface
// Type Alias
type Point = {
x: number
y: number
}
type ID = string | number
// Interface
interface IPoint {
x: number
y: number
}
// Key differences:
// 1. Interface supports Declaration Merging
interface IPoint {
z?: number // adds z to existing IPoint
}
// 2. Type Alias supports union, intersection, and primitive types
type StringOrNumber = string | number
type WithTimestamp = Point & { createdAt: Date }
// 3. Both can be implemented by classes
class MyPoint implements IPoint {
x = 0
y = 0
}
// When to choose:
// - Public APIs for libraries/SDKs: Interface (extensible via declaration merging)
// - Internal app types: Type Alias (more flexible)
// - Object shapes only: either works fine
1.6 Literal Types and Template Literal Types
// Literal types
type Direction = 'north' | 'south' | 'east' | 'west'
type DiceRoll = 1 | 2 | 3 | 4 | 5 | 6
const move = (dir: Direction) => console.log(dir)
move('north') // OK
// move('up') // Error
// Template literal types (TypeScript 4.1+)
type EventName = 'click' | 'focus' | 'blur'
type Handler = `on${Capitalize<EventName>}`
// Result: 'onClick' | 'onFocus' | 'onBlur'
type CSSUnit = 'px' | 'em' | 'rem' | 'vw' | 'vh'
type CSSValue = `${number}${CSSUnit}`
// Examples: '16px', '1.5em', '100vw', etc.
// Practical example: API route types
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE'
type Endpoint = '/users' | '/posts' | '/comments'
type APIRoute = `${HttpMethod} ${Endpoint}`
// Result: 12 combinations like 'GET /users', 'POST /posts', etc.
2. Object Types and Utility Types
2.1 Advanced Object Type Features
interface User {
readonly id: number // read-only
name: string
email?: string // optional property
[key: string]: unknown // index signature
}
const user: User = { id: 1, name: 'Alice' }
// user.id = 2 // Error: readonly
2.2 Built-in Utility Types
interface Todo {
id: number
title: string
completed: boolean
description: string
}
// Partial<T> - makes all properties optional
type PartialTodo = Partial<Todo>
// Required<T> - makes all properties required
type RequiredTodo = Required<Partial<Todo>>
// Readonly<T> - makes all properties read-only
type ReadonlyTodo = Readonly<Todo>
// Pick<T, K> - picks specific properties
type TodoPreview = Pick<Todo, 'id' | 'title'>
// Omit<T, K> - omits specific properties
type TodoWithoutDescription = Omit<Todo, 'description'>
// Record<K, V> - creates a key-value map type
type TodoMap = Record<number, Todo>
// Exclude<T, U> - removes U from the union T
type T1 = Exclude<string | number | boolean, boolean> // string | number
// Extract<T, U> - keeps only U from the union T
type T2 = Extract<string | number | boolean, string | boolean> // string | boolean
// NonNullable<T> - removes null and undefined
type T3 = NonNullable<string | null | undefined> // string
// ReturnType<T> - extracts the return type of a function
function getUser() {
return { id: 1, name: 'Alice' }
}
type UserReturn = ReturnType<typeof getUser> // { id: number; name: string }
// Parameters<T> - extracts parameter types as a tuple
function createUser(name: string, age: number, active: boolean) {}
type CreateUserParams = Parameters<typeof createUser> // [string, number, boolean]
// ConstructorParameters<T> - extracts constructor parameter types
class ApiClient {
constructor(
public baseUrl: string,
public timeout: number
) {}
}
type ClientParams = ConstructorParameters<typeof ApiClient> // [string, number]
2.3 Mapped Types
// Basic mapped type
type Flags<T> = {
[K in keyof T]: boolean
}
interface Config {
darkMode: string
notifications: string
analytics: string
}
type ConfigFlags = Flags<Config>
// { darkMode: boolean; notifications: boolean; analytics: boolean }
// Adding/removing modifiers
type Mutable<T> = {
-readonly [K in keyof T]: T[K] // removes readonly
}
type AllRequired<T> = {
[K in keyof T]-?: T[K] // removes optionality
}
// Key remapping (TypeScript 4.1+)
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K]
}
type UserGetters = Getters<{ name: string; age: number }>
// { getName: () => string; getAge: () => number }
// Key remapping for filtering
type OnlyStrings<T> = {
[K in keyof T as T[K] extends string ? K : never]: T[K]
}
2.4 Conditional Types
// Basic conditional type
type IsString<T> = T extends string ? true : false
type A = IsString<string> // true
type B = IsString<number> // false
// Distributive conditional types (distributed over unions)
type ToArray<T> = T extends any ? T[] : never
type C = ToArray<string | number> // string[] | number[]
// The infer keyword - infers a type within a conditional type
type UnpackArray<T> = T extends Array<infer Item> ? Item : T
type D = UnpackArray<string[]> // string
type E = UnpackArray<number> // number (not an array, returned as-is)
type UnpackPromise<T> = T extends Promise<infer R> ? R : T
type F = UnpackPromise<Promise<string>> // string
// Implementing ReturnType manually
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never
type G = MyReturnType<() => string> // string
// Deep type manipulation
type DeepPartial<T> = T extends object ? { [K in keyof T]?: DeepPartial<T[K]> } : T
3. Generics
3.1 Generic Functions
// Basic generic function
function identity<T>(value: T): T {
return value
}
const str = identity<string>('hello') // string
const num = identity(42) // number (inferred)
// Multiple type parameters
function pair<A, B>(first: A, second: B): [A, B] {
return [first, second]
}
// Return the first element of an array
function first<T>(arr: T[]): T | undefined {
return arr[0]
}
3.2 Generic Interfaces and Classes
// Generic interface
interface Repository<T> {
findById(id: number): Promise<T | null>
findAll(): Promise<T[]>
create(item: Omit<T, 'id'>): Promise<T>
update(id: number, item: Partial<T>): Promise<T>
delete(id: number): Promise<void>
}
// Generic class
class Stack<T> {
private items: T[] = []
push(item: T): void {
this.items.push(item)
}
pop(): T | undefined {
return this.items.pop()
}
peek(): T | undefined {
return this.items[this.items.length - 1]
}
get size(): number {
return this.items.length
}
isEmpty(): boolean {
return this.items.length === 0
}
}
const stack = new Stack<number>()
stack.push(1)
stack.push(2)
console.log(stack.pop()) // 2
3.3 Generic Constraints (extends)
// Constraining with extends
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key]
}
const user = { name: 'Alice', age: 30 }
const name = getProperty(user, 'name') // string
// getProperty(user, 'email') // Error: 'email' is not keyof typeof user
// Interface constraint
interface HasId {
id: number
}
function findById<T extends HasId>(items: T[], id: number): T | undefined {
return items.find((item) => item.id === id)
}
// Multiple constraints
interface Serializable {
serialize(): string
}
function processAndSerialize<T extends HasId & Serializable>(item: T): string {
return `${item.id}: ${item.serialize()}`
}
3.4 Practical Generic Patterns
// API Response type pattern
interface ApiResponse<T> {
data: T
status: number
message: string
timestamp: string
}
interface PaginatedResponse<T> extends ApiResponse<T[]> {
pagination: {
page: number
limit: number
total: number
totalPages: number
}
}
// Repository pattern implementation
class UserRepository implements Repository<User> {
private baseUrl = '/api/users'
async findById(id: number): Promise<User | null> {
const res = await fetch(`${this.baseUrl}/${id}`)
if (!res.ok) return null
return res.json()
}
async findAll(): Promise<User[]> {
const res = await fetch(this.baseUrl)
return res.json()
}
async create(item: Omit<User, 'id'>): Promise<User> {
const res = await fetch(this.baseUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(item),
})
return res.json()
}
async update(id: number, item: Partial<User>): Promise<User> {
const res = await fetch(`${this.baseUrl}/${id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(item),
})
return res.json()
}
async delete(id: number): Promise<void> {
await fetch(`${this.baseUrl}/${id}`, { method: 'DELETE' })
}
}
4. Advanced Type Techniques
4.1 Union Types and Intersection Types
// Union Types
type StringOrNumber = string | number
type Nullable<T> = T | null
type Optional<T> = T | null | undefined
// Intersection Types
interface Named {
name: string
}
interface Aged {
age: number
}
type Person = Named & Aged
const person: Person = { name: 'Alice', age: 30 }
// Useful intersection pattern
type WithTimestamp<T> = T & {
createdAt: Date
updatedAt: Date
}
4.2 Discriminated Unions (Tagged Union / Algebraic Data Types)
// Each case has a unique literal type discriminant
interface LoadingState {
status: 'loading'
}
interface SuccessState<T> {
status: 'success'
data: T
}
interface ErrorState {
status: 'error'
error: Error
}
type AsyncState<T> = LoadingState | SuccessState<T> | ErrorState
// Type narrowing via discriminant
function renderState<T>(state: AsyncState<T>): string {
switch (state.status) {
case 'loading':
return 'Loading...'
case 'success':
return `Data: ${JSON.stringify(state.data)}`
case 'error':
return `Error: ${state.error.message}`
}
}
// Redux action pattern
type Action =
| { type: 'INCREMENT'; payload: number }
| { type: 'DECREMENT'; payload: number }
| { type: 'RESET' }
function reducer(state: number, action: Action): number {
switch (action.type) {
case 'INCREMENT':
return state + action.payload
case 'DECREMENT':
return state - action.payload
case 'RESET':
return 0
}
}
4.3 Type Guards
// typeof type guard
function processValue(value: string | number): string {
if (typeof value === 'string') {
return value.toUpperCase() // value: string
}
return value.toFixed(2) // value: number
}
// instanceof type guard
class Cat {
meow() {
return 'Meow'
}
}
class Dog {
bark() {
return 'Woof'
}
}
function speak(animal: Cat | Dog): string {
if (animal instanceof Cat) {
return animal.meow() // animal: Cat
}
return animal.bark() // animal: Dog
}
// in operator type guard
interface Fish {
swim(): void
}
interface Bird {
fly(): void
}
function move(animal: Fish | Bird): void {
if ('swim' in animal) {
animal.swim() // animal: Fish
} else {
animal.fly() // animal: Bird
}
}
// Custom type guard (is keyword)
function isString(value: unknown): value is string {
return typeof value === 'string'
}
function isUser(obj: unknown): obj is User {
return typeof obj === 'object' && obj !== null && 'id' in obj && 'name' in obj
}
// Assertion Function (TypeScript 3.7+)
function assertIsString(value: unknown): asserts value is string {
if (typeof value !== 'string') {
throw new Error(`Expected string, got ${typeof value}`)
}
}
const maybeString: unknown = 'hello'
assertIsString(maybeString)
console.log(maybeString.toUpperCase()) // narrowed to string after assertion
4.4 Overload Signatures
// Function overloads
function process(x: number): number
function process(x: string): string
function process(x: number[]): number[]
function process(x: number | string | number[]): number | string | number[] {
if (typeof x === 'number') return x * 2
if (typeof x === 'string') return x.toUpperCase()
return x.map((n) => n * 2)
}
const r1 = process(10) // number
const r2 = process('hello') // string
const r3 = process([1, 2, 3]) // number[]
// Method overloads
class EventEmitter {
on(event: 'data', listener: (data: Buffer) => void): this
on(event: 'end', listener: () => void): this
on(event: 'error', listener: (err: Error) => void): this
on(event: string, listener: (...args: any[]) => void): this {
// implementation
return this
}
}
5. Classes and Decorators
5.1 Access Modifiers and Class Features
class BankAccount {
readonly accountNumber: string
private _balance: number
protected owner: string
constructor(accountNumber: string, owner: string, initialBalance = 0) {
this.accountNumber = accountNumber
this.owner = owner
this._balance = initialBalance
}
get balance(): number {
return this._balance
}
private validateAmount(amount: number): void {
if (amount <= 0) throw new Error('Amount must be greater than 0')
}
deposit(amount: number): void {
this.validateAmount(amount)
this._balance += amount
}
withdraw(amount: number): void {
this.validateAmount(amount)
if (amount > this._balance) throw new Error('Insufficient funds')
this._balance -= amount
}
static createSavingsAccount(owner: string): BankAccount {
const accountNumber = `SAV-${Date.now()}`
return new BankAccount(accountNumber, owner, 0)
}
}
// Constructor parameter shorthand (Parameter Properties)
class Point {
constructor(
public readonly x: number,
public readonly y: number
) {}
distance(other: Point): number {
return Math.sqrt((this.x - other.x) ** 2 + (this.y - other.y) ** 2)
}
}
5.2 Abstract Classes
abstract class Shape {
abstract area(): number
abstract perimeter(): number
// Shared implementation
toString(): string {
return `Area: ${this.area().toFixed(2)}, Perimeter: ${this.perimeter().toFixed(2)}`
}
}
class Circle extends Shape {
constructor(private radius: number) {
super()
}
area(): number {
return Math.PI * this.radius ** 2
}
perimeter(): number {
return 2 * Math.PI * this.radius
}
}
class Rectangle extends Shape {
constructor(
private width: number,
private height: number
) {
super()
}
area(): number {
return this.width * this.height
}
perimeter(): number {
return 2 * (this.width + this.height)
}
}
5.3 Decorators
Decorators require experimentalDecorators: true in tsconfig.json.
// Class decorator
function Singleton<T extends new (...args: any[]) => {}>(constructor: T) {
let instance: InstanceType<T>
return class extends constructor {
constructor(...args: any[]) {
if (instance) return instance
super(...args)
instance = this as any
}
}
}
@Singleton
class AppConfig {
readonly version = '1.0.0'
}
// Method decorator
function Log(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const original = descriptor.value
descriptor.value = function (...args: any[]) {
console.log(`Calling: ${propertyKey}(${JSON.stringify(args)})`)
const result = original.apply(this, args)
console.log(`Returned: ${JSON.stringify(result)}`)
return result
}
return descriptor
}
// Property decorator
function Validate(min: number, max: number) {
return function (target: any, propertyKey: string) {
let value: number
Object.defineProperty(target, propertyKey, {
get: () => value,
set: (newValue: number) => {
if (newValue < min || newValue > max) {
throw new Error(`${propertyKey} must be between ${min} and ${max}`)
}
value = newValue
},
})
}
}
class Product {
name: string
@Validate(0, 1000000)
price: number
constructor(name: string, price: number) {
this.name = name
this.price = price
}
@Log
getInfo(): string {
return `${this.name}: $${this.price}`
}
}
6. Module System and Declaration Files
6.1 ES Modules in TypeScript
// Named exports
export interface User {
id: number
name: string
}
export function createUser(name: string): User {
return { id: Date.now(), name }
}
// Default export
export default class UserService {
private users: User[] = []
add(user: User): void {
this.users.push(user)
}
}
// Re-exports
export { User as UserType } from './types'
export * from './utils'
export * as helpers from './helpers'
// Imports
import UserService, { User, createUser } from './user'
import type { User as UserType } from './user' // type-only import (removed at runtime)
6.2 Declaration Files (.d.ts)
// globals.d.ts - global type declarations
declare global {
interface Window {
analytics: {
track(event: string, properties?: Record<string, unknown>): void
}
}
}
// Module declaration (augmenting untyped JS libraries)
declare module 'legacy-lib' {
export function doSomething(value: string): number
export const VERSION: string
}
// File type declarations (e.g., SVG files)
declare module '*.svg' {
const content: string
export default content
}
declare module '*.png' {
const content: string
export default content
}
6.3 Key tsconfig.json Options
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"lib": ["ES2022", "DOM"],
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"paths": {
"@/*": ["./src/*"],
"@components/*": ["./src/components/*"]
},
"baseUrl": ".",
"outDir": "./dist",
"rootDir": "./src",
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowImportingTsExtensions": true,
"resolveJsonModule": true
}
}
7. Practical Patterns
7.1 Builder Pattern with Method Chaining
class QueryBuilder<T> {
private conditions: string[] = []
private orderByClause?: string
private limitValue?: number
private offsetValue?: number
where(condition: string): this {
this.conditions.push(condition)
return this
}
orderBy(field: keyof T & string, direction: 'ASC' | 'DESC' = 'ASC'): this {
this.orderByClause = `${field} ${direction}`
return this
}
limit(n: number): this {
this.limitValue = n
return this
}
offset(n: number): this {
this.offsetValue = n
return this
}
build(): string {
let query = 'SELECT * FROM table'
if (this.conditions.length > 0) {
query += ` WHERE ${this.conditions.join(' AND ')}`
}
if (this.orderByClause) {
query += ` ORDER BY ${this.orderByClause}`
}
if (this.limitValue !== undefined) {
query += ` LIMIT ${this.limitValue}`
}
if (this.offsetValue !== undefined) {
query += ` OFFSET ${this.offsetValue}`
}
return query
}
}
// Usage example
const query = new QueryBuilder<User>()
.where('age > 18')
.where('active = true')
.orderBy('name')
.limit(10)
.offset(0)
.build()
7.2 Result Type (Either Monad Pattern)
type Ok<T> = { success: true; data: T }
type Err<E> = { success: false; error: E }
type Result<T, E = Error> = Ok<T> | Err<E>
function ok<T>(data: T): Ok<T> {
return { success: true, data }
}
function err<E>(error: E): Err<E> {
return { success: false, error }
}
// Usage example
async function fetchUser(id: number): Promise<Result<User>> {
try {
const response = await fetch(`/api/users/${id}`)
if (!response.ok) {
return err(new Error(`HTTP ${response.status}`))
}
const data = await response.json()
return ok(data)
} catch (e) {
return err(e instanceof Error ? e : new Error('Unknown error'))
}
}
// Caller is forced to handle errors explicitly
const result = await fetchUser(1)
if (result.success) {
console.log(result.data.name)
} else {
console.error(result.error.message)
}
7.3 Branded Types (Type-Safe IDs)
// Branded types - same underlying type but mutually incompatible
type UserId = number & { readonly _brand: 'UserId' }
type PostId = number & { readonly _brand: 'PostId' }
type CommentId = number & { readonly _brand: 'CommentId' }
// Factory functions
function asUserId(id: number): UserId {
return id as UserId
}
function asPostId(id: number): PostId {
return id as PostId
}
function getUser(id: UserId): User {
/* ... */ return {} as User
}
function getPost(id: PostId): void {
/* ... */
}
const userId = asUserId(1)
const postId = asPostId(1)
getUser(userId) // OK
// getUser(postId) // Error: PostId is not assignable to UserId
// getUser(1) // Error: number is not assignable to UserId
7.4 The satisfies Operator (TypeScript 4.9+)
// satisfies - validates the type while preserving the inferred type
const palette = {
red: [255, 0, 0],
green: '#00ff00',
blue: [0, 0, 255],
} satisfies Record<string, string | number[]>
// Because of satisfies:
palette.red.map((x) => x * 2) // OK: inferred as number[]
palette.green.toUpperCase() // OK: inferred as string
// No type assertion needed while still enforcing the contract
const config = {
port: 3000,
host: 'localhost',
features: {
auth: true,
logging: false,
},
} satisfies {
port: number
host: string
features: Record<string, boolean>
}
8. Quiz
Quiz 1: unknown vs any
Identify which line causes a compile error in the following code.
const a: any = 'hello'
const b: unknown = 'world'
console.log(a.toUpperCase()) // (1)
console.log(b.toUpperCase()) // (2)
if (typeof b === 'string') {
console.log(b.toUpperCase()) // (3)
}
Answer: Line (2)
Explanation: The unknown type cannot be accessed without a type guard. any disables type checking entirely, so (1) compiles fine. unknown requires narrowing before use, so (2) is a compile error. (3) is fine because the typeof guard has already narrowed the type.
Quiz 2: Composing Utility Types
Express the following type using utility types.
interface Article {
id: number
title: string
content: string
author: string
publishedAt: Date
updatedAt: Date
}
// Goal: make all fields optional except id, publishedAt, and updatedAt
type ArticleUpdateInput = /* ??? */
Answer: Partial<Omit<Article, 'id' | 'publishedAt' | 'updatedAt'>>
Explanation: First use Omit to remove the non-editable fields, then use Partial to make all remaining fields optional. Utility types can be composed this way.
Quiz 3: Generic Constraints
Add generic constraints so the following function compiles without errors.
function mergeObjects<T, U>(target: T, source: U): T & U {
return { ...target, ...source }
}
// Requirement: both T and U must be object types
Answer: function mergeObjects<T extends object, U extends object>(target: T, source: U): T & U
Explanation: Using extends object prevents passing primitive types (string, number, etc.) and only allows object types. Using Record<string, unknown> is sometimes more explicit.
Quiz 4: Discriminated Union Exhaustiveness
Explain why adding a new shape Triangle to the union does not cause a compile error below, and how to fix it so it does.
type Shape = Circle | Square // After adding Triangle: Circle | Square | Triangle
function getArea(shape: Shape): number {
if (shape.kind === 'circle') return Math.PI * shape.radius ** 2
if (shape.kind === 'square') return shape.side ** 2
return 0 // This prevents the compile error
}
Answer: Replace return 0 with the never exhaustiveness check pattern: const _: never = shape; throw new Error(...)
Explanation: return 0 silently handles all unmatched cases. Using the never exhaustiveness check makes the compiler complain when Triangle is added, because shape would no longer be never at that point.
Quiz 5: infer and Conditional Types
Predict the result of the following types.
type Flatten<T> = T extends Array<infer Item> ? Item : T
type A = Flatten<string[]> // ?
type B = Flatten<number[][]> // ?
type C = Flatten<string> // ?
type D = Flatten<string | number[]> // ?
Answer: A = string, B = number[], C = string, D = string | number
Explanation: Flatten unwraps only one level of array. B sees number[][] as Array<number[]> and returns number[]. D distributes over the union: Flatten<string> | Flatten<number[]> = string | number.
Summary
TypeScript's type system is not mere syntax sugar. When used correctly, it catches runtime errors at compile time and unlocks full IDE autocompletion and refactoring support.
Key takeaways:
- Prefer
unknownoveranyto maintain type safety - Use utility types and mapped types for DRY type definitions
- Model state machines safely with discriminated unions
- Write reusable, type-safe code with generics
- Use the
satisfiesoperator to validate and infer types simultaneously