TypeScript高级类型技巧
TypeScript的类型系统非常强大,提供了许多高级特性来解决复杂的类型问题。
条件类型
条件类型允许我们基于类型关系创建新的类型,类似于JavaScript中的条件表达式。
基本条件类型
条件类型的基本语法是 T extends U ? X : Y
,意思是"如果类型T可以赋值给类型U,则结果类型为X,否则为Y":
// 基本条件类型
type IsString<T> = T extends string ? true : false;
// 使用条件类型
type A = IsString<string>; // true
type B = IsString<number>; // false
type C = IsString<"hello">; // true(字面量类型也是string的子类型)
分配条件类型
当条件类型应用于联合类型时,它会分配到联合的每个成员:
type ToArray<T> = T extends any ? T[] : never;
// 分配条件类型
type StringOrNumberArray = ToArray<string | number>;
// 等同于: string[] | number[]
// 避免分配行为
type ToArrayNonDist<T> = [T] extends [any] ? T[] : never;
type Result = ToArrayNonDist<string | number>;
// (string | number)[]
条件类型中的类型推断
使用infer
关键字,我们可以在条件类型中推断和捕获类型的一部分:
// 从函数类型中提取返回类型
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : any;
function greet(): string {
return "你好,世界!";
}
type GreetReturn = ReturnType<typeof greet>; // string
// 从数组类型中提取元素类型
type ElementType<T> = T extends (infer U)[] ? U : never;
type NumberType = ElementType<number[]>; // number
type StringType = ElementType<string[]>; // string
// 从Promise中提取值类型
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
type ResolvedType = UnwrapPromise<Promise<string>>; // string
type NonPromiseType = UnwrapPromise<number>; // number
条件类型链
条件类型可以链式使用,类似于if-else if-else
链:
type TypeName<T> =
T extends string ? "string" :
T extends number ? "number" :
T extends boolean ? "boolean" :
T extends undefined ? "undefined" :
T extends Function ? "function" :
"object";
// 使用条件类型链
type T0 = TypeName<string>; // "string"
type T1 = TypeName<42>; // "number"
type T2 = TypeName<true>; // "boolean"
type T3 = TypeName<() => void>; // "function"
type T4 = TypeName<{}>; // "object"
映射类型
映射类型允许我们基于旧类型创建新类型,通过转换每个属性。
基本映射类型
基本映射类型语法使用[P in keyof T]
来遍历类型的所有属性:
// 将所有属性设为只读
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
// 将所有属性设为可选
type Partial<T> = {
[P in keyof T]?: T[P];
};
interface User {
id: number;
name: string;
email: string;
}
// 使用映射类型
type ReadonlyUser = Readonly<User>;
// { readonly id: number; readonly name: string; readonly email: string; }
type PartialUser = Partial<User>;
// { id?: number; name?: string; email?: string; }
键重映射
TypeScript 4.1引入了键重映射,允许我们在映射类型中转换属性名:
// 添加前缀
type Prefix<T, P extends string> = {
[K in keyof T as `${P}${string & K}`]: T[K];
};
// 过滤属性
type FilterByValueType<T, ValueType> = {
[K in keyof T as T[K] extends ValueType ? K : never]: T[K];
};
interface Person {
name: string;
age: number;
address: string;
isActive: boolean;
}
// 使用键重映射
type PrefixedPerson = Prefix<Person, "get">;
// { getName: string; getAge: number; getAddress: string; getIsActive: boolean; }
type StringProperties = FilterByValueType<Person, string>;
// { name: string; address: string; }
映射修饰符
我们可以使用+
和-
修饰符来添加或移除readonly
和?
修饰符:
// 移除只读属性
type Mutable<T> = {
-readonly [P in keyof T]: T[P];
};
// 移除可选属性
type Required<T> = {
[P in keyof T]-?: T[P];
};
// 添加只读和可选
type ReadonlyAndPartial<T> = {
+readonly [P in keyof T]+?: T[P];
};
interface Config {
readonly host: string;
port: number;
timeout: number;
retries?: number;
}
// 使用映射修饰符
type MutableConfig = Mutable<Config>;
// { host: string; port: number; timeout: number; retries?: number; }
type RequiredConfig = Required<Config>;
// { host: string; port: number; timeout: number; retries: number; }
type SafeConfig = ReadonlyAndPartial<Config>;
// { readonly host?: string; readonly port?: number; readonly timeout?: number; readonly retries?: number; }
索引类型
索引类型允许我们在编译时检查使用动态属性名的代码。
索引类型查询操作符
keyof
操作符获取一个对象类型的所有键的联合:
interface User {
id: number;
name: string;
email: string;
}
// 使用keyof
type UserKeys = keyof User; // "id" | "name" | "email"
// 使用索引类型查询
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user: User = {
id: 1,
name: "张三",
email: "zhangsan@example.com"
};
// 类型安全的属性访问
const userName = getProperty(user, "name"); // 类型是string
const userId = getProperty(user, "id"); // 类型是number
// 错误:参数"age"不能赋给参数"K"
// getProperty(user, "age");
索引访问类型
使用T[K]
语法可以查找类型T
上属性K
的类型:
interface Person {
name: string;
age: number;
address: {
street: string;
city: string;
country: string;
};
}
// 使用索引访问类型
type PersonName = Person["name"]; // string
type PersonAge = Person["age"]; // number
type AddressProperty = Person["address"]; // { street: string; city: string; country: string; }
type CityType = Person["address"]["city"]; // string
// 使用联合类型作为索引
type NameOrAge = Person["name" | "age"]; // string | number
// 使用keyof
type AllPersonProps = Person[keyof Person]; // string | number | { street: string; city: string; country: string; }
类型工具与组合
TypeScript提供了许多内置工具类型,我们也可以创建自定义工具类型。
内置工具类型
TypeScript提供了许多有用的工具类型:
// Partial - 将所有属性变为可选
interface User {
id: number;
name: string;
email: string;
}
type PartialUser = Partial<User>;
// { id?: number; name?: string; email?: string; }
// Required - 将所有属性变为必需
interface Config {
host?: string;
port?: number;
timeout?: number;
}
type RequiredConfig = Required<Config>;
// { host: string; port: number; timeout: number; }
// Readonly - 将所有属性变为只读
type ReadonlyUser = Readonly<User>;
// { readonly id: number; readonly name: string; readonly email: string; }
// Record - 创建具有指定键和值类型的对象类型
type PageInfo = Record<string, { title: string; content: string }>;
// { [key: string]: { title: string; content: string } }
// Pick - 从类型中选择指定的属性
type UserBasicInfo = Pick<User, "id" | "name">;
// { id: number; name: string; }
// Omit - 从类型中排除指定的属性
type UserWithoutEmail = Omit<User, "email">;
// { id: number; name: string; }
// Exclude - 从联合类型中排除指定的类型
type Status = "pending" | "approved" | "rejected";
type NonRejectedStatus = Exclude<Status, "rejected">;
// "pending" | "approved"
// Extract - 从联合类型中提取指定的类型
type PositiveStatus = Extract<Status, "approved" | "pending">;
// "approved" | "pending"
// NonNullable - 排除null和undefined
type MaybeString = string | null | undefined;
type DefinitelyString = NonNullable<MaybeString>;
// string
// ReturnType - 获取函数返回类型
function createUser(name: string, email: string): User {
return { id: Date.now(), name, email };
}
type CreateUserReturn = ReturnType<typeof createUser>;
// User
// Parameters - 获取函数参数类型的元组
type CreateUserParams = Parameters<typeof createUser>;
// [name: string, email: string]
// ConstructorParameters - 获取构造函数参数类型的元组
class Person {
constructor(name: string, age: number) {}
}
type PersonConstructorParams = ConstructorParameters<typeof Person>;
// [name: string, age: number]
// InstanceType - 获取构造函数实例类型
type PersonInstance = InstanceType<typeof Person>;
// Person
自定义工具类型
我们可以创建自定义工具类型来满足特定需求:
// 深度Partial
type DeepPartial<T> = T extends object ? {
[P in keyof T]?: DeepPartial<T[P]>;
} : T;
interface NestedObject {
name: string;
settings: {
theme: string;
notifications: {
email: boolean;
sms: boolean;
push: boolean;
};
};
}
// 使用DeepPartial
const partialSettings: DeepPartial<NestedObject> = {
name: "我的应用",
settings: {
notifications: {
email: true
// sms和push是可选的
}
// theme是可选的
}
};
// 只读递归
type DeepReadonly<T> = {
readonly [P in keyof T]: T[P] extends object ? DeepReadonly<T[P]> : T[P];
};
// 非空类型
type NonNullableProperties<T> = {
[P in keyof T]: NonNullable<T[P]>;
};
// 函数属性
type FunctionPropertyNames<T> = {
[K in keyof T]: T[K] extends Function ? K : never;
}[keyof T];
// 非函数属性
type NonFunctionPropertyNames<T> = {
[K in keyof T]: T[K] extends Function ? never : K;
}[keyof T];
interface Example {
name: string;
age: number;
greet: () => void;
calculate: (x: number) => number;
}
type FuncProps = FunctionPropertyNames<Example>; // "greet" | "calculate"
type DataProps = NonFunctionPropertyNames<Example>; // "name" | "age"
类型守卫与类型收窄
类型守卫允许我们在运行时检查类型,并在特定的代码块中收窄类型。
用户定义的类型守卫
使用类型谓词(parameterName is Type
)创建自定义类型守卫:
// 定义类型
interface Bird {
fly(): void;
layEggs(): void;
}
interface Fish {
swim(): void;
layEggs(): void;
}
// 类型守卫函数
function isFish(pet: Bird | Fish): pet is Fish {
return (pet as Fish).swim !== undefined;
}
// 使用类型守卫
function move(pet: Bird | Fish) {
if (isFish(pet)) {
// 在这个块中,TypeScript知道pet是Fish类型
pet.swim();
} else {
// 在这个块中,TypeScript知道pet是Bird类型
pet.fly();
}
}
// 泛型类型守卫
function isArray<T>(value: unknown): value is T[] {
return Array.isArray(value);
}
function processValue<T>(value: unknown) {
if (isArray<number>(value)) {
// value的类型是number[]
value.map(n => n * 2);
}
}
类型收窄
TypeScript提供了多种方式来收窄类型:
// typeof类型守卫
function padLeft(value: string, padding: string | number) {
if (typeof padding === "number") {
// 在这个块中,padding的类型是number
return " ".repeat(padding) + value;
}
// 在这个块中,padding的类型是string
return padding + value;
}
// instanceof类型守卫
class Bird {
fly() {
console.log("鸟儿飞行");
}
}
class Fish {
swim() {
console.log("鱼儿游泳");
}
}
function move(animal: Bird | Fish) {
if (animal instanceof Bird) {
// 在这个块中,animal的类型是Bird
animal.fly();
} else {
// 在这个块中,animal的类型是Fish
animal.swim();
}
}
// in操作符类型守卫
interface Admin {
name: string;
privileges: string[];
}
interface Employee {
name: string;
startDate: Date;
}
type UnknownEmployee = Employee | Admin;
function printEmployeeInfo(emp: UnknownEmployee) {
console.log(`姓名: ${emp.name}`);
if ("privileges" in emp) {
// 在这个块中,emp的类型是Admin
console.log(`特权: ${emp.privileges.join(", ")}`);
}
if ("startDate" in emp) {
// 在这个块中,emp的类型是Employee
console.log(`入职日期: ${emp.startDate.toLocaleDateString()}`);
}
}
// 可辨识联合类型
interface Circle {
kind: "circle";
radius: number;
}
interface Square {
kind: "square";
sideLength: number;
}
interface Rectangle {
kind: "rectangle";
width: number;
height: number;
}
type Shape = Circle | Square | Rectangle;
function getArea(shape: Shape): number {
switch (shape.kind) {
case "circle":
// 在这个case中,shape的类型是Circle
return Math.PI * shape.radius ** 2;
case "square":
// 在这个case中,shape的类型是Square
return shape.sideLength ** 2;
case "rectangle":
// 在这个case中,shape的类型是Rectangle
return shape.width * shape.height;
default:
// 穷尽检查
const _exhaustiveCheck: never = shape;
return _exhaustiveCheck;
}
}
模板字面量类型
TypeScript 4.1引入了模板字面量类型,允许我们基于字符串字面量类型创建新的字符串字面量类型。
基本模板字面量类型
模板字面量类型使用反引号语法,类似于JavaScript的模板字符串:
// 基本模板字面量类型
type World = "world";
type Greeting = `hello ${World}`; // "hello world"
// 使用联合类型
type Color = "red" | "blue" | "green";
type Quantity = "one" | "two" | "three";
type ColoredQuantity = `${Quantity} ${Color}`;
// "one red" | "one blue" | "one green" | "two red" | ... | "three green"
// 使用类型参数
type Prefix<P extends string, T extends string> = `${P}${T}`;
type Prefixed = Prefix<"get", "Name" | "Age" | "Email">;
// "getName" | "getAge" | "getEmail"
内置字符串操作类型
TypeScript提供了几个内置的字符串操作类型:
// Uppercase - 转换为大写
type UppercaseGreeting = Uppercase<"hello">; // "HELLO"
// Lowercase - 转换为小写
type LowercaseGreeting = Lowercase<"HELLO">; // "hello"
// Capitalize - 首字母大写
type CapitalizedGreeting = Capitalize<"hello">; // "Hello"
// Uncapitalize - 首字母小写
type UncapitalizedGreeting = Uncapitalize<"Hello">; // "hello"
// 组合使用
type PropEventName<T extends string> = `on${Capitalize<T>}`;
type ButtonEvent = PropEventName<"click" | "mousedown" | "mouseup">;
// "onClick" | "onMousedown" | "onMouseup"
模板字面量的高级应用
模板字面量类型可以与其他类型特性结合使用:
// 创建对象属性的getter类型
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
interface Person {
name: string;
age: number;
address: string;
}
type PersonGetters = Getters<Person>;
// { getName: () => string; getAge: () => number; getAddress: () => string; }
// 创建事件处理器类型
type EventHandler<T extends string> = {
[K in T as `on${Capitalize<K>}`]: (event: any) => void;
};
type ButtonEvents = EventHandler<"click" | "mouseenter" | "mouseleave">;
// { onClick: (event: any) => void; onMouseenter: (event: any) => void; onMouseleave: (event: any) => void; }
// 创建状态变更类型
type State = {
loading: boolean;
error: Error | null;
data: any;
};
type SetActions<T> = {
[K in keyof T as `set${Capitalize<string & K>}`]: (value: T[K]) => void;
};
type StateActions = SetActions<State>;
// { setLoading: (value: boolean) => void; setError: (value: Error | null) => void; setData: (value: any) => void; }
高级类型挑战
以下是一些高级类型编程挑战,可以帮助你提升TypeScript类型系统的理解和应用能力。
实现深度只读
创建一个类型,使对象及其所有嵌套属性都变为只读:
type DeepReadonly<T> = {
readonly [P in keyof T]: T[P] extends object
? T[P] extends Function
? T[P]
: DeepReadonly<T[P]>
: T[P];
};
// 测试
interface NestedObject {
a: number;
b: string;
c: {
d: boolean;
e: {
f: string[];
};
};
}
const original: NestedObject = {
a: 1,
b: "hello",
c: {
d: true,
e: {
f: ["world"]
}
}
};
const readonlyObj: DeepReadonly<NestedObject> = original;
// 以下操作都会导致错误
// readonlyObj.a = 2;
// readonlyObj.c.d = false;
// readonlyObj.c.e.f.push("!");
实现类型过滤
创建一个类型,从对象类型中过滤出符合特定条件的属性:
type FilterByValueType<T, ValueType> = {
[K in keyof T as T[K] extends ValueType ? K : never]: T[K];
};
// 测试
interface Mixed {
name: string;
age: number;
isActive: boolean;
tags: string[];
metadata: {
createdAt: Date;
};
process(): void;
}
type StringProps = FilterByValueType<Mixed, string>;
// { name: string; }
type FunctionProps = FilterByValueType<Mixed, Function>;
// { process: () => void; }
type ObjectProps = FilterByValueType<Mixed, object>;
// { tags: string[]; metadata: { createdAt: Date; }; }
实现联合类型转交叉类型
创建一个类型,将联合类型转换为交叉类型:
type UnionToIntersection<U> =
(U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never;
// 测试
type Union = { a: string } | { b: number } | { c: boolean };
type Intersection = UnionToIntersection<Union>;
// { a: string } & { b: number } & { c: boolean }
// 使用交叉类型
const obj: Intersection = {
a: "hello",
b: 42,
c: true
};
实现元组转联合类型
创建一个类型,将元组类型转换为联合类型:
type TupleToUnion<T extends any[]> = T[number];
// 测试
type Tuple = [string, number, boolean];
type Union = TupleToUnion<Tuple>;
// string | number | boolean
实现联合类型转元组
这是一个更复杂的挑战,将联合类型转换为元组类型:
// 注意:这是一个高级实现,需要深入理解TypeScript类型系统
type UnionToTuple<T, L = LastOf<T>> =
[T] extends [never]
? []
: [...UnionToTuple<Exclude<T, L>>, L];
type LastOf<T> =
UnionToIntersection<T extends any ? () => T : never> extends () => infer R
? R
: never;
// 辅助类型
type UnionToIntersection<U> =
(U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never;
// 测试
type Union = "a" | "b" | "c";
type Tuple = UnionToTuple<Union>;
// ["a", "b", "c"] 或其他排列,取决于实现
总结
TypeScript的高级类型系统提供了强大的工具,使我们能够创建精确、安全且可重用的类型定义。通过掌握条件类型、映射类型、索引类型、类型守卫和模板字面量类型等高级特性,你可以构建更加健壮的应用程序,减少运行时错误,并提高代码的可维护性。
随着你对TypeScript类型系统的深入理解,你将能够解决越来越复杂的类型问题,创建更加精确的类型定义,并充分利用TypeScript提供的类型安全保障。类型编程虽然有时看起来复杂,但掌握这些技巧将极大地提升你的TypeScript开发能力。