📚 TypeScript 教程系列
入门与配置 基础类型与变量声明 函数 流程控制与运算符 集合类型 异步编程与错误处理 接口与类 泛型与类型组合 (本文)高级类型 模块、装饰器与工程化 ⚠️ 来源声明 :本文内容参考自 菜鸟教程 TypeScript 教程 ,仅供学习交流,版权归原作者所有。 TypeScript 的类型系统不止能描述单一类型,还能通过泛型让代码与具体类型解耦,再借助 type 别名、联合类型、交叉类型、字面量类型等把多个类型组合出精确的约束。本篇从泛型出发,依次串联类型别名、联合与交叉、字面量、类型守卫、可选链与空值合并、模板字面量类型,构建一套可复用且类型安全的类型组合工具链。
泛型(Generics) 泛型是一种语言特性,它允许在定义函数、类、接口等时用占位符表示类型而非具体类型。这一功能有助于编写可重用、灵活且类型安全的代码。
其主要目的是处理不特定类型的数据,使代码能适配多种数据类型而不丧失类型检查。
泛型的三大优势 代码重用: 可编写与特定类型无关的通用代码,提升复用性。类型安全: 编译阶段进行类型检查,规避运行时类型错误。抽象性: 支持编写更抽象通用的代码,适应不同数据类型和数据结构。泛型标识符 约定俗成的标识符包括 T(Type)、U、V 等,实际上任何合法标识符均可使用。
T — 代表 “Type”,最常见的泛型类型参数名。
1 2 3 function identity<T>(arg : T): T { return arg; }
K, V — 表示键(Key)和值(Value)的泛型类型参数。
1 2 3 4 interface KeyValuePair <K, V> { key : K; value : V; }
E — 表示数组元素的泛型类型参数。
1 2 3 function printArray<E>(arr : E[]): void { arr.forEach (item => console .log (item)); }
R — 表示函数返回值的泛型类型参数。
1 2 3 function getResult<R>(value : R): R { return value; }
U, V — 通常表示第二、第三个泛型类型参数。
1 2 3 function combine<U, V>(first : U, second : V): string { return `${first} ${second} ` ; }
这些标识符仅为惯例,实际可选用任何符合命名规范的名称。关键在于代码的可读性,因此建议使用描述性名称以便理解其用途。
泛型函数 使用泛型创建可处理不同类型的函数。
1 2 3 4 5 6 7 8 9 function identity<T>(arg : T): T { return arg; }let result = identity<string >("Hello" );console .log (result);let numberResult = identity<number >(42 );console .log (numberResult);
上述 identity 是一个泛型函数,通过 <T> 表示泛型类型,参数和返回值均为类型 T。调用时用尖括号 <> 显式指定类型——第一次指定 string,第二次指定 number。
泛型接口 使用泛型定义接口,使其成员能适配任意类型。
1 2 3 4 5 6 7 interface Pair <T, U> { first : T; second : U; }let pair : Pair <string , number > = { first : "hello" , second : 42 };console .log (pair);
此处定义了泛型接口 Pair,带有两个类型参数 T 和 U。创建对象 pair 时,first 为字符串类型,second 为数字类型。
泛型类 泛型同样可应用于类的实例变量和方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 class Box <T> { private value : T; constructor (value : T ) { this .value = value; } getValue (): T { return this .value ; } }let stringBox = new Box <string >("TypeScript" );console .log (stringBox.getValue ());
Box 是一个泛型类,使用 <T> 表示泛型类型。构造函数和 getValue 方法均使用泛型类型 T。通过 Box<string> 实例化后创建了存储字符串的 Box 实例。
多类型参数 泛型可以同时接受多个类型参数,用逗号分隔,常用于需要同时处理多种类型的场景。
1 2 3 4 5 6 function merge<T, U>(first : T, second : U): [T, U] { return [first, second]; }const merged = merge<string , number >("id" , 42 );console .log (merged);
1 2 3 4 5 6 7 interface KeyValuePair <K, V> { key : K; value : V; }const kv : KeyValuePair <string , boolean > = { key : "enabled" , value : true };console .log (kv);
泛型约束(extends) 有时需要限制泛型的类型范围,可通过约束实现。
1 2 3 4 5 6 7 8 9 10 11 interface Lengthwise { length : number ; }function logLength<T extends Lengthwise >(arg : T): void { console .log (arg.length ); }logLength ("hello" );logLength (42 );
泛型函数 logLength 要求参数类型 T 必须实现 Lengthwise 接口(即具有 length 属性)。因此 logLength("hello") 可正确调用,而 logLength(42) 会报错,因为数字没有 length 属性。
约束也可以引用其他类型参数,例如从对象中安全取属性:
1 2 3 4 5 6 7 function getProperty<T, K extends keyof T>(obj : T, key : K) { return obj[key]; }const person = { name : "Alice" , age : 25 };console .log (getProperty (person, "name" ));
泛型与默认类型 可为泛型设置默认类型,在不指定类型参数时自动使用默认类型。
1 2 3 4 5 6 function defaultValue<T = string >(arg : T): T { return arg; }let result1 = defaultValue ("hello" );let result2 = defaultValue (42 );
函数 defaultValue 的泛型参数 T 默认类型为 string。若未显式指定类型则使用默认类型。第一个调用中 result1 被推断为 string,第二个调用中 result2 被推断为 number。
默认类型在泛型接口与泛型类型别名中同样适用:
1 2 3 4 5 6 7 interface Response <T = any > { code : number ; data : T; }const r1 : Response = { code : 200 , data : "ok" }; const r2 : Response <number > = { code : 200 , data : 1 };
type 别名 type 别名(Type Alias)用于为现有类型创建别名,让代码更简洁、更易读。通过类型别名,可以为复杂类型定义一个简短的名字,提高代码的可维护性。
type 别名使用 type 关键字定义,它只是为现有类型起了一个新名字,不会创建新的类型。
type 别名工作原理 步骤 说明 原始复杂类型 type User = { name: string; age: number; email: string; }定义 type 别名 type UserID = User; 或 type ID = string | number;使用 var id: UserID; var uid: ID;
type 别名的应用场景包括:基础类型别名、联合类型别名、函数类型别名、泛型类型别名等。
为什么需要 type 别名 当代码中多次使用同一个复杂类型时,每次都写完整类型会很冗长。type 别名可以为复杂类型定义一个简洁的名字,让代码更易读、更易维护。特别是在使用联合类型、函数类型、元组等复杂类型时,type 别名非常有用。
基本用法 使用 type 关键字为类型定义别名。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 type ID = string | number ;type Point = { x : number ; y : number };var userId : ID = "123" ;var productId : ID = 456 ;var point : Point = { x : 10 , y : 20 };console .log ("用户ID: " + userId);console .log ("产品ID: " + productId);console .log ("坐标: " + JSON .stringify (point));
输出:
1 2 3 用户ID: 123 产品ID: 456 坐标: { "x" : 10 , "y" : 20 }
类型别名只是给类型起了个新名字,编译后不会产生实际代码,它完全用于开发时的类型检查。
接口 vs 类型别名 类型别名和接口非常相似,都可以用来定义对象类型,但有一些细微区别。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 type PersonType = { name : string ; age : number ; };interface PersonInterface { name : string ; age : number ; }var person1 : PersonType = { name : "Alice" , age : 25 };var person2 : PersonInterface = { name : "Bob" , age : 30 };console .log ("PersonType: " + JSON .stringify (person1));console .log ("PersonInterface: " + JSON .stringify (person2));
type 与 interface 的关键区别:
对比项 type interface 适用范围 可定义联合、元组、函数、基本类型等任意类型 主要用于定义对象类型 声明合并 不支持(同名重复定义会报错) 支持(同名接口自动合并) 类实现 可以被类 implements 可以被类 implements 继承/扩展 通过 & 交叉扩展 通过 extends 继承 计算属性 支持(映射类型、模板字面量等) 受限
type 可以定义任何类型(联合类型、元组、函数类型等),而接口主要用于定义对象类型。接口支持声明合并,type 不支持。
类型别名与联合类型 类型别名非常适合定义联合类型,让代码更清晰。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 type Status = "pending" | "success" | "error" ;type Result = string | number | boolean ;function getStatus (status : Status ): void { console .log ("状态: " + status); }getStatus ("success" );getStatus ("error" );var result : Result = "hello" ; result = 42 ;console .log ("结果: " + result);
输出:
1 2 3 状态: success 状态: error 结果: 42
联合类型别名常用于定义状态码、错误类型、API 返回值等场景。
类型别名与元组 类型别名也可以用于定义元组类型。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 type Coordinate = [number , number ];type NameAge = [string , number ];var coord : Coordinate = [10 , 20 ];var person : NameAge = ["Alice" , 25 ];console .log ("坐标: " + coord);console .log ("信息: " + person[0 ] + ", " + person[1 ]);
元组类型别名让元组的使用更加清晰,避免了每次都需要写出完整元组类型的问题。
类型别名与函数 类型别名可以简化函数类型的声明。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 type Callback = (result : string ) => void ;type MathOperation = (a : number , b : number ) => number ;var add : MathOperation = function (a, b ) { return a + b; };var multiply : MathOperation = function (a, b ) { return a * b; };console .log ("加法: " + add (2 , 3 ));console .log ("乘法: " + multiply (4 , 5 ));
输出:
函数类型别名让函数类型声明更简洁,特别是在需要多次使用相同函数签名时。
类型别名与泛型 类型别名可以配合泛型使用,创建可复用的类型定义。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 type Result <T> = { success : boolean ; data ?: T; error ?: string };type Pair <K, V> = { key : K; value : V };var result : Result <string > = { success : true , data : "Hello" };var pair : Pair <string , number > = { key : "age" , value : 25 };console .log ("结果: " + JSON .stringify (result));console .log ("键值对: " + JSON .stringify (pair));
输出:
1 2 结果: { "success" : true , "data" : "Hello" } 键值对: { "key" : "age" , "value" : 25 }
泛型类型别名可以适应不同的数据类型,提高代码的复用性。
类型别名与映射类型 类型别名可以与映射类型结合,创建强大的类型转换。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 type Readonly <T> = { readonly [P in keyof T]: T[P] };type Partial <T> = { [P in keyof T]?: T[P] };interface User { name : string ; age : number ; }var readonlyUser : Readonly <User > = { name : "Alice" , age : 25 };var partialUser : Partial <User > = { name : "Bob" };console .log ("只读用户: " + JSON .stringify (readonlyUser));console .log ("部分用户: " + JSON .stringify (partialUser));
映射类型是 TypeScript 非常强大的特性,可以基于现有类型创建新的类型变化。
type 别名注意事项 同一个 type 别名不能重复定义(接口可以声明合并)。 type 别名在编译后不产生任何代码。 不要滥用类型别名,简洁明了的代码更好。 当类型需要复用,或者类型名称过长时使用 type 别名。对于简单的对象类型,可以根据团队习惯选择 type 或接口。
联合类型(Union Types) 联合类型(Union Types)可以通过管道(|)让变量设置为多种类型,赋值时可以根据设置的类型来赋值。只能赋值指定的类型,如果赋值其它类型就会报错。
语法格式:Type1|Type2|Type3
基本实例 1 2 3 4 5 var val :string |number val = 12 console .log ("数字为 " + val) val = "Runoob" console .log ("字符串为 " + val)
编译以上代码,得到以下 JavaScript 代码:
1 2 3 4 5 var val; val = 12 ;console .log ("数字为 " + val); val = "Runoob" ;console .log ("字符串为 " + val);
输出结果为:
如果赋值其它类型就会报错,例如 var val:string|number 后写 val = true 会出现编译错误。
联合类型作为函数参数 联合类型也可以作为函数参数使用:
1 2 3 4 5 6 7 8 9 10 11 12 13 function disp (name :string |string [] ) { if (typeof name == "string" ) { console .log (name) } else { var i; for (i = 0 ;i<name.length ;i++) { console .log (name[i]) } } }disp ("Runoob" )console .log ("输出数组...." )disp (["Runoob" ,"Google" ,"Taobao" ,"Facebook" ])
编译以上代码,得到以下 JavaScript 代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 function disp (name ) { if (typeof name == "string" ) { console .log (name); } else { var i; for (i = 0 ; i < name.length ; i++) { console .log (name[i]); } } }disp ("Runoob" );console .log ("输出数组...." );disp (["Runoob" , "Google" , "Taobao" , "Facebook" ]);
输出结果为:
1 2 3 4 5 6 Runoob 输出数组. . . . Runoob Google Taobao Facebook
联合类型数组 也可以将数组声明为联合类型:
1 2 3 4 5 6 7 8 9 10 11 12 var arr :number []|string [];var i :number ; arr = [1 ,2 ,4 ]console .log ("**数字数组**" )for (i = 0 ;i<arr.length ;i++) { console .log (arr[i]) } arr = ["Runoob" ,"Google" ,"Taobao" ]console .log ("**字符串数组**" )for (i = 0 ;i<arr.length ;i++) { console .log (arr[i]) }
编译以上代码,得到以下 JavaScript 代码:
1 2 3 4 5 6 7 8 9 10 11 12 var arr;var i; arr = [1 , 2 , 4 ];console .log ("**数字数组**" );for (i = 0 ; i < arr.length ; i++) { console .log (arr[i]); } arr = ["Runoob" , "Google" , "Taobao" ];console .log ("**字符串数组**" );for (i = 0 ; i < arr.length ; i++) { console .log (arr[i]); }
输出结果为:
1 2 3 4 5 6 7 8 ** 数字数组** 1 2 4 ** 字符串数组** Runoob Google Taobao
交叉类型(Intersection Types) 交叉类型(Intersection Types)将多个类型合并成一个新类型,新类型包含所有成员的类型。这类似于面向对象中的多重继承,让一个类型可以拥有多个类型的特性。
交叉类型使用 & 符号连接多个类型,表示新类型包含所有类型的成员。
交叉类型工作原理 类型 A (Person) 类型 B (Worker) 合并 A & B (Employee) name: string company: string name: string age: number salary: number age: number company: string salary: number
交叉类型使用场景:
类型组合 — 合并多个类型特性Mixin 模式 — 组合多个类的功能替代接口继承 — 更简洁的类型组合为什么需要交叉类型 在开发中,一个类型往往需要拥有多个类型的特性。例如,一个员工既是一个人(Person),也是一个工作者(Worker),需要同时具有两者的属性。交叉类型让我们可以将多个类型合并成一个,满足这种需求。
基本语法 使用 & 符号组合多个类型。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 interface Person { name : string ; age : number ; }interface Worker { company : string ; salary : number ; }type Employee = Person & Worker ;var employee : Employee = { name : "Alice" , age : 25 , company : "Google" , salary : 100000 };console .log ("员工: " + JSON .stringify (employee));
输出:
1 员工: { "name" : "Alice" , "age" : 25 , "company" : "Google" , "salary" : 100000 }
交叉类型 A & B 意味着新类型同时具有 A 和 B 的所有属性,缺一不可。
交叉类型与接口继承 交叉类型可以替代接口的多重继承。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 interface A { a : string ; }interface B { b : number ; }interface AB extends A, B { c : boolean ; }type ABType = A & B & { c : boolean };var obj : ABType = { a : "hello" , b : 42 , c : true };console .log ("对象: " + JSON .stringify (obj));
输出:
1 对象: { "a" : "hello" , "b" : 42 , "c" : true }
交叉类型比接口继承更简洁,特别是当需要继承多个类型且需要添加额外属性时。
类型混合(Mixin 模式) 使用交叉类型实现 Mixin 模式,可以动态组合类的功能。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 type Constructor = new (...args : any []) => {};function Timestamped <T extends Constructor >(Base : T) { return class extends Base { timestamp = Date .now (); }; }function Serializable <T extends Constructor >(Base : T) { return class extends Base { serialize ( ) { return JSON .stringify (this ); } }; }class User { name : string ; constructor (name : string ) { this .name = name; } }var TimestampedUser = Timestamped (User );var SerializableUser = Serializable (User );var FullUser = Serializable (Timestamped (User ));var user = new FullUser ("Alice" );console .log ("时间戳: " + user.timestamp );console .log ("序列化: " + user.serialize ());
Mixin 是一种强大的模式,可以在不修改原有类的情况下为其添加新功能。
交叉类型与联合类型 交叉类型和联合类型结合时需要特别注意优先级。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 type StringOrNumber = string | number ;type Both = string & number ;type A = { a : string };type B = { b : number };type C = { c : boolean };type Combined = (A | B) & C;var obj : Combined = { a : "hello" , c : true };console .log ("组合: " + JSON .stringify (obj));
输出:
1 组合: { "a" : "hello" , "c" : true }
不兼容的类型进行交叉会得到 never 类型。例如 string & number 是无效的。
交叉类型注意事项 不兼容类型: 交叉不兼容的类型会得到 never。优先级: 联合类型的优先级高于交叉类型。方法冲突: 如果两个类型有同名的方法,需要手动处理冲突。交叉类型适合组合多个接口或类型,当需要多重继承时,优先使用交叉类型而非接口继承。
字面量类型(Literal Types) 字面量类型(Literal Types)是 TypeScript 中一种表示特定值而不是通用类型的特性。它允许我们将变量类型限制为具体的值,而不是宽泛的 string、number 等类型。这提供了更精确的类型控制,使代码更加类型安全。
字面量类型对比 写法 可赋值范围 var direction: string;任何字符串 “up”、“down”、“abc”… var direction: "up" | "down" | "left" | "right"只能赋这四个值之一 赋值 direction = "upup" 编译错误!
字面量类型种类:
字符串字面量 :"up" | "down" | "left" — 限制特定字符串数字字面量 :1 | 2 | 3 | 100 | 200 — 限制特定数字布尔字面量 :true | false — true 或 false模板字面量 :`on${string}` — 动态生成字面量类型是泛型类型(string、number)的具体值。例如 "up" 是 string 的子类型,只能赋值给它的值是特定的字符串。
字符串字面量类型 字符串字面量类型将变量的取值限制为特定的字符串。这在需要限制变量只能取某些固定值时非常有用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 var direction : "up" | "down" | "left" | "right" ; direction = "up" ;console .log ("方向: " + direction);type Status = "pending" | "active" | "completed" ;var currentStatus : Status = "active" ;console .log ("状态: " + currentStatus);
输出:
字符串字面量常用于定义枚举值、状态码、配置选项等需要限制为特定值的场景。
数字字面量类型 数字字面量类型将变量的取值限制为特定的数字。这在需要使用特定数值(如状态码、错误码)时非常有用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 var code : 200 | 404 | 500 ; code = 200 ;console .log ("状态码: " + code);type Weekday = 1 | 2 | 3 | 4 | 5 | 6 | 7 ;var today : Weekday = 1 ;console .log ("今天是星期: " + today);
数字字面量可以替代简单的枚举(enum)使用,尤其在只需要几个固定数值时。
布尔字面量类型 布尔字面量类型将变量的取值限制为 true 或 false。实际上,TypeScript 中的 boolean 类型就是 true | false 的别名。
1 2 3 4 5 6 7 8 9 var isActive : true | false ; isActive = true ;var flag : boolean = true ;console .log ("是否激活: " + isActive);
虽然 boolean 本身已经足够使用,但在需要明确区分 true 和 false 的类型时,可以使用 true 或 false 作为类型。
对象字面量类型 对象字面量类型可以定义对象的结构,并可以使用 readonly 修饰符将属性设为只读。这在需要创建不可变对象时非常有用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 type Point = { x : number ; y : number ; };var p : Point = { x : 10 , y : 20 };console .log ("点: " + JSON .stringify (p));type ReadonlyPoint = { readonly x : number ; readonly y : number ; };var rp : ReadonlyPoint = { x : 1 , y : 2 };console .log ("只读点: " + JSON .stringify (rp));
输出:
1 2 点: { "x" : 10 , "y" : 20 } 只读点: { "x" : 1 , "y" : 2 }
只读对象常用于定义配置对象、常量对象等不希望被修改的数据。
字面量类型与类型推断 TypeScript 会根据变量的声明方式自动推断为字面量类型。使用 const 声明的变量会被推断为具体的字面量类型,而不是宽泛的类型。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 var name = "Alice" ; var age = 25 ; var enabled = true ; console .log ("名字: " + name + ", 年龄: " + age + ", 启用: " + enabled);var colors = ["red" , "green" , "blue" ] as const ;console .log ("颜色: " + colors[0 ]);
as const 会将类型推断为最具体的字面量类型,并添加 readonly 修饰符。
实际应用:Redux Action 字面量类型在实际开发中有广泛的应用。下面展示如何使用字面量类型实现 Redux 风格的行为(Action)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 type Action = | { type : "increment" ; payload : number } | { type : "decrement" ; payload : number } | { type : "reset" };function reducer (action : Action ): void { switch (action.type ) { case "increment" : console .log ("增加: " + action.payload ); break ; case "decrement" : console .log ("减少: " + action.payload ); break ; case "reset" : console .log ("重置" ); break ; } }reducer ({ type : "increment" , payload : 5 });reducer ({ type : "reset" });
输出:
这种模式叫做 “可辨识联合”(Discriminated Union),是 TypeScript 中处理多种相关类型的最佳实践。
字面量类型注意事项 类型收窄: 使用 switch 或 if 判断时,TypeScript 会自动收窄字面量类型。类型别名: 建议使用 type 别名为常用的字面量类型创建别名。const 推断: const 声明的变量会自动推断为字面量类型。as const: 需要深度只读时使用 as const。在需要限制变量只能取特定值时,优先使用字面量类型而不是枚举。
类型守卫(Type Guards) 类型守卫(Type Guards)是 TypeScript 中非常重要的一种类型缩小机制。它允许开发者在运行时通过特定的条件检查,让 TypeScript 编译器能够准确推断出变量的具体类型。通过类型守卫,我们可以安全地访问联合类型变量中特定类型的属性和方法。
为什么需要类型守卫 在 TypeScript 中,一个变量可能被声明为多种类型的联合。当我们需要根据不同类型执行不同操作时,编译器无法自动判断当前的具体类型。类型守卫就是解决这个问题的关键机制。
类型守卫的核心原理是 “类型缩窄”(Type Narrowing):通过条件判断,TypeScript 会自动将联合类型缩小为具体的类型。
类型守卫方式总览 类型守卫方式 用法示例 typeoftypeof x === "string"instanceofx instanceof Array自定义守卫 x is String / value is Typein 属性检查"prop" in x
typeof 类型守卫 typeof 是最常用的类型守卫,用于检查原始类型(string、number、boolean 等)。它返回一个字符串,表示值的类型。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 function printValue (value : string | number ): void { if (typeof value === "string" ) { console .log ("字符串长度: " + value.length ); } else { console .log ("数字翻倍: " + (value * 2 )); } }printValue ("hello" );printValue (42 );
输出:
typeof 支持的类型:
“string” — 字符串类型“number” — 数字类型(包括 NaN 和 Infinity)“boolean” — 布尔类型“undefined” — 未定义类型“object” — 对象类型(注意:数组和 null 也会被识别为 “object”)“function” — 函数类型typeof 对于数组和 null 都会返回 “object”。如果需要精确区分数组和对象,需要使用其他方式。
instanceof 类型守卫 instanceof 用于检查对象是否是某个类的实例。它通过检查对象的原型链来判断类型。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 class Dog { bark (): void { console .log ("汪汪汪!" ); } }class Cat { meow (): void { console .log ("喵喵喵!" ); } }function makeSound (animal : Dog | Cat ): void { if (animal instanceof Dog ) { animal.bark (); } else { animal.meow (); } }makeSound (new Dog ());makeSound (new Cat ());
输出:
instanceof 检查的是对象的原型链,因此它只能用于类实例,不能用于接口和类型别名。
自定义类型守卫 当内置的 typeof 和 instanceof 无法满足需求时,可以创建自定义类型守卫函数。自定义类型守卫使用 value is Type 的返回类型语法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 function isString (value : any ): value is string { return typeof value === "string" ; }function isNumber (value : any ): value is number { return typeof value === "number" ; }function isArray (value : any ): value is any [] { return Array .isArray (value); }function processValue (value : string | number | any [] ): void { if (isString (value)) { console .log ("字符串转大写: " + value.toUpperCase ()); } else if (isNumber (value)) { console .log ("数字格式化: " + value.toFixed (2 )); } else if (isArray (value)) { console .log ("数组长度: " + value.length ); } }processValue ("hello" );processValue (3.14159 );processValue ([1 , 2 , 3 , 4 , 5 ]);
输出:
1 2 3 字符串转大写: HELLO 数字格式化: 3.14 数组长度: 5
自定义类型守卫的关键是返回类型 value is Type,这是 TypeScript 识别类型守卫的标志。
in 操作符类型守卫 in 操作符可以检查对象是否包含某个属性。在条件判断中使用 in,TypeScript 会自动缩小对象的类型范围。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 interface A { a : string ; }interface B { b : number ; }function process (obj : A | B ): void { if ("a" in obj) { console .log ("A 的属性 a: " + obj.a ); } else { console .log ("B 的属性 b: " + obj.b ); } }process ({ a : "hello" });process ({ b : 42 });
输出:
1 2 A 的属性 a : hello B 的属性 b : 42
可辨识联合与类型守卫 可辨识联合是一种强大的模式,它通过一个公共的 “标识” 属性来区分联合类型成员。结合 switch 语句或 if 判断,可以实现完整的类型守卫。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 interface Circle { kind : "circle" ; radius : number ; }interface Rectangle { kind : "rectangle" ; width : number ; height : number ; }interface Triangle { kind : "triangle" ; base : number ; height : number ; }type Shape = Circle | Rectangle | Triangle ;function getArea (shape : Shape ): number { switch (shape.kind ) { case "circle" : return Math .PI * shape.radius ** 2 ; case "rectangle" : return shape.width * shape.height ; case "triangle" : return 0.5 * shape.base * shape.height ; } }var circle = { kind : "circle" as const , radius : 5 };var rectangle = { kind : "rectangle" as const , width : 4 , height : 6 };var triangle = { kind : "triangle" as const , base : 3 , height : 4 };console .log ("圆形面积: " + getArea (circle).toFixed (2 ));console .log ("矩形面积: " + getArea (rectangle));console .log ("三角形面积: " + getArea (triangle));
输出:
1 2 3 圆形面积: 78.54 矩形面积: 24 三角形面积: 6
可辨识联合是 TypeScript 中最推荐使用的模式之一。它通过一个公共的字面量属性(通常是 kind 或 type)来区分不同的类型成员,使代码既类型安全又易于维护。
null 和 undefined 检查 处理可能为 null 或 undefined 的值时,直接的相等性检查也是有效的类型守卫。
1 2 3 4 5 6 7 8 9 10 function getLength (str : string | null ): number { if (str !== null ) { return str.length ; } return 0 ; }console .log (getLength ("hello" ));console .log (getLength (null ));
输出:
在启用 strictNullChecks 后,建议始终进行 null 检查。可以使用可选链(?.)和空值合并(??)来简化代码。
真值缩小 除了显式的类型检查,TypeScript 还会通过真值断言(Truthiness)来缩小类型范围。
1 2 3 4 5 6 7 function greet (name ?: string ): string { return name && "Hello, " + name; }console .log (greet ("RUNOOB" ));console .log (greet ());
输出:
1 2 Hello, RUNOOB Hello, undefined
类型守卫注意事项 类型守卫必须在条件分支中使用: 只有在使用类型守卫进行条件判断后,TypeScript 才会进行类型缩小。返回类型必须是类型谓词: 自定义类型守卫的返回类型必须是 value is Type 格式。可辨识联合是最佳实践: 对于复杂的联合类型,建议使用可辨识联合模式。注意类型收窄的完整性: 使用 switch 语句时,建议处理所有可能的分支。在处理联合类型时,优先考虑使用可辨识联合模式。它不仅代码更清晰,还能充分利用 TypeScript 的类型推断能力。
可选链 ?. 与空值合并 ?? 可选链(Optional Chaining)是 TypeScript 和 JavaScript 中一种安全的属性访问方式。它允许开发者以链式调用的方式安全地访问嵌套对象属性。当访问路径中的任意一个属性为 null 或 undefined 时,整个表达式会短路返回 undefined,而不会抛出错误。这极大地简化了深层嵌套对象的属性访问代码。
可选链工作原理 传统方式(冗长): 需要多层检查:var city = user && user.address && user.address.city; — 容易出错,代码冗余。
可选链方式(简洁): 一行搞定:var city = user?.address?.city; — 安全简洁,自动短路。
可选链的三种形式:
obj?.prop — 属性访问arr?.[0] — 数组访问obj?.method() — 方法调用可选链的核心是 “短路求值”:当链中某个属性的值为 null 或 undefined 时,整个表达式的结果立即返回 undefined,而不会继续访问后续属性。
基本语法 使用 ?. 运算符安全访问可能不存在的属性。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 var user = { name : "RUNOOB" , address : { city : "Beijing" } };var city1 = user && user.address && user.address .city ;var city2 = user?.address ?.city ;console .log ("传统方式: " + city1);console .log ("可选链: " + city2);
输出:
1 2 传统方式: Beijing 可选链: Beijing
当对象属性存在时,两种方式的结果相同。但可选链的代码更简洁,更易读。
处理不存在的属性 当访问路径中的属性不存在时,可选链会安全地返回 undefined,而不会抛出错误。这对于处理来自 API 的数据或用户表单输入特别有用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 var user = { name : "RUNOOB" };var city = user?.address ?.city ;var country = user?.address ?.country ?.name ;console .log ("城市: " + city);console .log ("国家: " + country);
输出:
1 2 城市: undefined 国家: undefined
可选链只会在属性访问时返回 undefined,不会创建新的对象或属性。
可选链与数组结合 可选链可以与数组下标访问结合使用,安全地访问数组中的元素。使用 ?.[index] 语法,可以在数组元素不存在时返回 undefined。
1 2 3 4 5 6 7 8 9 10 11 12 13 var users = [ { name : "Alice" }, { name : "Bob" } ];var firstUser = users?.[0 ]?.name ;var tenthUser = users?.[9 ]?.name ;console .log ("第一个用户: " + firstUser);console .log ("第十个用户: " + tenthUser);
输出:
1 2 第一个用户: Alice 第十个用户: undefined
?.[index] 与 ?. 的区别在于:前者用于数组,后者用于对象属性。
可选链与方法调用 可选链可以用于安全地调用可能不存在的方法。使用 ?.() 语法,如果方法不存在则返回 undefined,而不会抛出错误。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 var user = { name : "Alice" , greet : function ( ) { return "Hello, " + this .name ; } };var message1 = user.greet ?.();var message2 = user.sayHello ?.();console .log ("greet: " + message1);console .log ("sayHello: " + message2);
输出:
1 2 greet: Hello, Alice sayHello: undefined
这在处理可选的回调函数或事件处理程序时特别有用。
可选链赋值 可选链也可以用于赋值操作,但需要注意其行为:可选链赋值只会修改已存在的路径,不会自动创建中间对象。
1 2 3 4 5 6 7 8 var user = { name : "Alice" }; user?.address ?.city = "Beijing" ;console .log ("用户: " + JSON .stringify (user));
输出:
可选链赋值不能用于创建新属性。如果需要创建嵌套对象,应该使用传统方式先创建中间对象。
空值合并运算符 ?? 可选链经常与空值合并运算符 ?? 结合使用。这种组合可以在属性不存在时提供默认值,使代码更加健壮。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 var user = { name : "Alice" };var city = user?.address ?.city ?? "未知城市" ;console .log ("城市: " + city);var country = user && user.address && user.address .country ? user.address .country : "未知国家" ;console .log ("国家: " + country);
输出:
?? 与 || 的关键区别:
运算符 触发默认值的条件 示例 ??值为 null 或 undefined 0 ?? "默认" → 0` `
?? 只在值为 null 或 undefined 时使用默认值,而 || 会在值为 falsy(0、“”、false)时也使用默认值。在处理数字或字符串时,应优先使用 ??。
可选链注意事项 短路求值: 可选链遇到 null 或 undefined 会立即返回,不会继续访问后续属性。不能创建属性: 可选链赋值不会自动创建中间对象。与空值合并配合: 建议始终使用 ?? 提供默认值,而少用 ||。性能考虑: 虽然可选链更安全,但在属性一定存在的情况下,直接访问性能更好。对于来自外部数据(如 API 响应、用户输入)的属性,优先使用可选链进行安全访问。结合空值合并运算符,可以写出既安全又简洁的代码。
模板字面量类型(Template Literal Types) 模板字面量类型基于字符串字面量类型构建,支持通过插值生成新的字符串类型。这让 TypeScript 能够对字符串进行更精确的类型检查,适用于事件名、路径、类名等场景。
模板字面量类型工作原理 输入类型 模板 输出类型 type Event = "click" | "focus"`on${Capitalize<Event>}`"onClick" "onFocus"type Method = "get" | "post"`${Method}:/${string}`"get:/..." "post:/..."
内置工具类型:
工具类型 作用 示例 Uppercase全部大写 "hello" → "HELLO"Lowercase全部小写 "HELLO" → "hello"Capitalize首字母大写 "hello" → "Hello"Uncapitalize首字母小写 "Hello" → "hello"
为什么需要模板字面量类型 在开发中,我们经常需要处理格式化的字符串,如事件名(onClick)、API 路径(get:/users)、CSS 类名(btn-primary-md)等。使用普通的 string 类型无法精确描述这些格式,而模板字面量类型让我们可以精确地定义这些字符串的类型,大大增强了 TypeScript 的类型安全性,减少了运行时错误。
模板字面量类型使用反引号(`)和 ${} 插值语法来定义字符串类型,类似于 JavaScript 的模板字符串,但用在类型层面。
基本语法 1 2 3 4 5 6 7 8 9 10 type World = "world" ;type Greeting = `Hello ${World} ` ;var greeting : Greeting = "Hello world" ;console .log ("问候: " + greeting);
输出:
模板字面量类型会将插值的位置替换为实际的字符串,生成新的字面量类型。
内置工具类型 TypeScript 提供了四个内置的工具类型来处理字符串大小写。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 type UpperHello = Uppercase <"hello" >; type LowerHELLO = Lowercase <"HELLO" >; type CapitalizedHello = Capitalize <"hello" >; type UncapitalizedHello = Uncapitalize <"Hello" >; console .log ("Uppercase: " + UpperHello );console .log ("Lowercase: " + LowerHELLO );console .log ("Capitalize: " + CapitalizedHello );console .log ("Uncapitalize: " + UncapitalizedHello );
输出:
1 2 3 4 Uppercase: HELLOLowercase: helloCapitalize: HelloUncapitalize: hello
这些工具类型在处理事件名、方法名等需要统一格式的场景非常有用。
事件类型 使用模板字面量类型可以精确地定义事件名称的类型。
1 2 3 4 5 6 7 8 9 10 11 type EventName = `on${Capitalize<string >} ` ;type Handler = `handle${Capitalize<string >} ` ;var clickEvent : EventName = "onClick" ;var focusEvent : EventName = "onFocus" ;var handler : Handler = "handleSubmit" ;console .log ("事件: " + clickEvent);console .log ("处理器: " + handler);
输出:
1 2 事件: onClick 处理器: handleSubmit
使用模板字面量类型后,像 onclick(小写)这样的错误格式会被 TypeScript 拒绝。
路径类型 使用模板字面量类型可以精确地定义 API 路径的类型。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 type HttpMethod = "get" | "post" | "put" | "delete" ;type ApiEndpoint = `/${string } ` ; type ApiPath = `${HttpMethod} ${ApiEndpoint} ` ;var getUsers : ApiPath = "get/users" ;var createUser : ApiPath = "post/users" ;console .log ("路径: " + getUsers);console .log ("路径: " + createUser);
输出:
1 2 路径: get/ users 路径: post/ users
像 users(没有方法前缀)这样的路径会被 TypeScript 报错。
复杂示例:组合多个联合类型 模板字面量类型可以组合多个联合类型,生成所有可能的组合。
1 2 3 4 5 6 7 8 9 10 11 type Row = `row${number } ` ; type Variant = "primary" | "secondary" ;type Size = "sm" | "md" | "lg" ;type ClassName = `btn-${Variant} -${Size} ` ;var className : ClassName = "btn-primary-md" ;console .log ("类名: " + className);
输出:
模板字面量类型会自动展开所有组合。如果联合类型有很多选项,生成的类型可能会非常庞大。
自定义工具类型 可以创建自己的模板字面量工具类型。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 type Prefix <T extends string , P extends string > = `${P} ${Capitalize<T>} ` ;type Suffix <T extends string , S extends string > = `${Capitalize<T>} ${S} ` ;type HandlerName = Prefix <"click" , "on" >;type ButtonId = Suffix <"submit" , "Btn" >;var handler : HandlerName = "onClick" ;var id : ButtonId = "SubmitBtn" ;console .log ("处理器: " + handler);console .log ("ID: " + id);
输出:
1 2 处理器: onClick ID: SubmitBtn
模板字面量类型可以与泛型结合,创建可复用的工具类型。
与 keyof 组合 模板字面量类型常与 keyof、映射类型配合,生成基于对象键的新类型,例如把属性名重写为 getter/setter 形式。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 type Getters <T> = { [K in keyof T as `get${Capitalize<string & K>} ` ]: () => T[K]; };interface Person { name : string ; age : number ; }type PersonGetters = Getters <Person >;var getters : PersonGetters = { getName : () => "Alice" , getAge : () => 25 };console .log ("名字: " + getters.getName ());console .log ("年龄: " + getters.getAge ());
输出:
通过 as 子句配合模板字面量类型,可以在映射类型中重写键名,从而派生出 get/set/on 等约定式 API 类型。
模板字面量类型注意事项 插值类型: 模板中的 ${} 可以是具体字符串、联合类型、string、number 等。组合数量: 组合多个联合类型时,生成的类型可能非常大。大小写处理: 使用内置工具类型处理字符串大小写。使用模板字面量类型处理事件名、路径、类名等有固定格式的字符串,可以获得更好的类型安全。在需要格式化字符串的场景,优先使用模板字面量类型来获得编译期的类型检查。
小结 本篇从泛型出发,串联起 TypeScript 类型组合的核心能力:
泛型 通过类型参数 <T> 让函数、接口、类与具体类型解耦,配合 extends 约束和默认类型实现可复用的类型安全代码。type 别名 为复杂类型起简洁名字,能表达联合、元组、函数、泛型与映射类型,与 interface 在对象类型上各有侧重。联合类型 用 | 表示 “或”,交叉类型用 & 表示 “且”,二者可叠加出灵活的类型组合,注意不兼容交叉会得到 never。字面量类型 把取值限定为具体值,配合 as const 与可辨识联合,是实现精确状态建模的关键。类型守卫 通过 typeof、instanceof、in、自定义 value is Type 与真值缩小,在条件分支中安全地缩窄类型。可选链 ?. 与空值合并 ?? 让深层属性访问与默认值处理既安全又简洁,优于传统的 && 与 ||。模板字面量类型 在类型层面拼接字符串,配合 Uppercase/Lowercase/Capitalize/Uncapitalize 与 keyof 映射,可生成事件名、路径、getter 等约定式类型。掌握这些类型组合工具,就能在不牺牲运行时性能的前提下,让编译器帮你挡住大量低级错误,写出既灵活又严谨的 TypeScript 代码。