読み込み中...
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.
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 let you write functions and types that work with any type while preserving type information through the call.
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 | undefinedUse 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 assignabletype 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" };TypeScript ships with several built-in utility types. Understanding how they work internally deepens your grasp of the type system.
// 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 };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 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>; // falseinfer Keywordinfer 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>>>; // stringWhen 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)[]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"// 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"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 }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 }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 UserIdThis pattern is especially useful for IDs, validated strings, and domain-specific values.
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;
}
}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 QueryBuilder<Selected extends string = never> = {
select<K extends string>(column: K): QueryBuilder<Selected | K>;
where(condition: string): QueryBuilder<Selected>;
execute(): Promise<Record<Selected, unknown>[]>;
};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 readonlytype 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 }); // Errorsatisfies for validation without widening: const config = { ... } satisfies Configunknown over any: Forces explicit narrowingas const for literal types: const routes = ["home", "about"] as constTypeScript'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.
コメント