⌨️

[SAMPLE] TypeScript Type System Deep Dive

TypeScript Type System Deep Dive

TypeScript's type system is one of the most powerful among mainstream programming languages. This guide explores advanced type features that enable you to write safer, more expressive code.

Beyond Basic Types

If you already understand string, number, boolean, interfaces, and simple generics, you are ready for this deep dive. We will cover the type-level programming features that distinguish TypeScript experts from beginners.

Generics: The Foundation

Generics let you write functions and types that work with any type while preserving type information through the call.

Basic Generic Function

function first<T>(array: T[]): T | undefined { return array[0]; } const num = first([1, 2, 3]); // number | undefined const str = first(["a", "b", "c"]); // string | undefined

Constrained Generics

Use extends to restrict what types a generic can accept:

function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] { return obj[key]; } const user = { name: "Alice", age: 30 }; getProperty(user, "name"); // string getProperty(user, "age"); // number getProperty(user, "email"); // Error: "email" is not assignable

Generic Defaults

type ApiResponse<T = unknown> = { data: T; status: number; message: string; }; // T defaults to unknown when not specified const response: ApiResponse = { data: null, status: 200, message: "OK" };

Utility Types

TypeScript ships with several built-in utility types. Understanding how they work internally deepens your grasp of the type system.

Essential Utility Types

// Partial<T> - all properties optional type Partial<T> = { [P in keyof T]?: T[P] }; // Required<T> - all properties required type Required<T> = { [P in keyof T]-?: T[P] }; // Readonly<T> - all properties readonly type Readonly<T> = { readonly [P in keyof T]: T[P] }; // Pick<T, K> - select specific properties type Pick<T, K extends keyof T> = { [P in K]: T[P] }; // Omit<T, K> - exclude specific properties type Omit<T, K extends keyof string | number | symbol> = Pick<T, Exclude<keyof T, K>>; // Record<K, V> - construct object type type Record<K extends keyof any, V> = { [P in K]: V };

Practical Example

interface User { id: string; name: string; email: string; createdAt: Date; } // For creating a user (no id or createdAt needed) type CreateUserInput = Omit<User, "id" | "createdAt">; // For updating a user (all fields optional except id) type UpdateUserInput = Pick<User, "id"> & Partial<Omit<User, "id">>;

Conditional Types

Conditional types let you express type-level if/else logic:

type IsString<T> = T extends string ? true : false; type A = IsString<"hello">; // true type B = IsString<42>; // false

The infer Keyword

infer extracts types from within conditional type patterns:

// Extract return type of a function type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never; // Extract element type of an array type ElementType<T> = T extends (infer E)[] ? E : never; type Nums = ElementType<number[]>; // number type Strs = ElementType<string[]>; // string // Extract promise value type Awaited<T> = T extends Promise<infer U> ? Awaited<U> : T; type Result = Awaited<Promise<Promise<string>>>; // string

Distributive Conditional Types

When a conditional type acts on a union, it distributes over each member:

type ToArray<T> = T extends any ? T[] : never; type Result = ToArray<string | number>; // = (string extends any ? string[] : never) | (number extends any ? number[] : never) // = string[] | number[] // To prevent distribution, wrap in tuple: type ToArrayNonDist<T> = [T] extends [any] ? T[] : never; type Result2 = ToArrayNonDist<string | number>; // = (string | number)[]

Template Literal Types

TypeScript can manipulate string types at the type level:

type EventName<T extends string> = `on${Capitalize<T>}`; type ClickEvent = EventName<"click">; // "onClick" type ChangeEvent = EventName<"change">; // "onChange" // Generate all possible combinations type Color = "red" | "blue"; type Size = "sm" | "lg"; type ClassName = `${Color}-${Size}`; // = "red-sm" | "red-lg" | "blue-sm" | "blue-lg"

Parsing Strings

// Extract parts of a string type type ExtractRouteParams<T extends string> = T extends `${string}:${infer Param}/${infer Rest}` ? Param | ExtractRouteParams<Rest> : T extends `${string}:${infer Param}` ? Param : never; type Params = ExtractRouteParams<"/users/:userId/posts/:postId">; // = "userId" | "postId"

Mapped Types

Transform existing types by iterating over their keys:

// Make all properties nullable type Nullable<T> = { [K in keyof T]: T[K] | null }; // Make all properties into getter functions 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 }

Key Remapping with as

// Remove readonly modifier type Mutable<T> = { -readonly [K in keyof T]: T[K] }; // Filter keys by value type type StringKeys<T> = { [K in keyof T as T[K] extends string ? K : never]: T[K]; }; interface Mixed { name: string; age: number; email: string; } type OnlyStrings = StringKeys<Mixed>; // { name: string; email: string }

Branded Types

TypeScript's structural type system means two types with the same shape are interchangeable. Branded types prevent this:

type UserId = string & { readonly __brand: "UserId" }; type PostId = string & { readonly __brand: "PostId" }; function createUserId(id: string): UserId { return id as UserId; } function getUser(id: UserId): void { /* ... */ } function getPost(id: PostId): void { /* ... */ } const userId = createUserId("user-123"); const postId = "post-456" as PostId; getUser(userId); // OK getUser(postId); // Error: PostId is not assignable to UserId

This pattern is especially useful for IDs, validated strings, and domain-specific values.

Discriminated Unions

Use a common literal property to narrow union types:

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

function assertNever(value: never): never { throw new Error(`Unexpected value: ${value}`); } 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 case is missing } }

Type-Level Programming Patterns

Builder Pattern with Types

type QueryBuilder<Selected extends string = never> = { select<K extends string>(column: K): QueryBuilder<Selected | K>; where(condition: string): QueryBuilder<Selected>; execute(): Promise<Record<Selected, unknown>[]>; };

Recursive Types

type DeepReadonly<T> = { readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K]; }; type NestedObj = { a: { b: { c: string } }; }; type ReadonlyNested = DeepReadonly<NestedObj>; // All levels are readonly

Type-Safe Event Emitter

type EventMap = { click: { x: number; y: number }; change: { value: string }; submit: { data: FormData }; }; class TypedEmitter<Events extends Record<string, any>> { on<K extends keyof Events>( event: K, handler: (payload: Events[K]) => void ): void { /* ... */ } emit<K extends keyof Events>( event: K, payload: Events[K] ): void { /* ... */ } } const emitter = new TypedEmitter<EventMap>(); emitter.on("click", ({ x, y }) => { /* x and y are typed */ }); emitter.emit("change", { value: "hello" }); // OK emitter.emit("change", { wrong: true }); // Error

Practical Tips

  1. Start with simple types and refine: Do not over-engineer types upfront
  2. Use satisfies for validation without widening: const config = { ... } satisfies Config
  3. Prefer unknown over any: Forces explicit narrowing
  4. Use as const for literal types: const routes = ["home", "about"] as const
  5. Avoid excessive type gymnastics: If a type is unreadable, simplify the design
  6. Test complex types: Use type assertions in tests to verify type behavior

Conclusion

TypeScript's type system is a language within a language. Mastering generics, conditional types, mapped types, and template literals unlocks the ability to create APIs that are both flexible and type-safe. The goal is not cleverness but clarity -- use advanced types to make invalid states unrepresentable and correct usage obvious.

0
0
0
0
投稿
0
フォロワー
0
いいね

プロパティ

ページ
GUIDE
ChatGPT転職
2024年6月26日
ジェフ・ベゾススティーブ・ジョブズ
英語