条件类型与类型守卫
核心问题解析
问题一:类型也能“if判断”吗?
在 JavaScript 中,我们用 if 根据值做逻辑分支:
if (typeof x === 'string') {
console.log(x.toUpperCase()); // ✅ 此时 x 被识别为 string
}TypeScript 的类型系统也支持类似的“逻辑判断”——这就是条件类型。
✅ 答案:是的,通过 T extends U ? X : Y 语法,TS 支持类型层面的“三元运算符”。
type IsString<T> = T extends string ? true : false;
type A = IsString<'hello'>; // true
type B = IsString<42>; // false🔍 这就是“类型编程”中的“条件逻辑”,让类型能根据输入动态选择。
问题二:instanceof 和 in 怎么影响类型?
看这个例子:
class Dog { bark() { console.log('woof'); } }
class Cat { meow() { console.log('meow'); } }
function makeSound(animal: Dog | Cat) {
if (animal instanceof Dog) {
animal.bark(); // ✅ 可调用
} else {
animal.meow(); // ✅ 可调用
}
}虽然 animal 类型是 Dog | Cat,但在 if 块内,TS 缩小了类型范围。
📌 机制:
instanceof:检查构造函数,TS 知道它能区分类实例in操作符:检查对象是否有某属性
if ('bark' in animal) {
animal.bark(); // ✅ TS 推断 animal: Dog
}✅ 答案:instanceof 和 in 是 TS 内置的类型守卫(Type Guard),能在运行时检查的基础上,实现类型缩小(Narrowing)。
问题三:为什么函数能“缩小”类型?
我们希望封装类型检查逻辑:
function isDog(animal: Dog | Cat): boolean {
return animal instanceof Dog;
}但直接使用会失败:
if (isDog(animal)) {
animal.bark(); // ❌ 报错!TS 不知道 isDog 能缩小类型
}✅ 解决方案:使用 is 类型谓词:
function isDog(animal: Dog | Cat): animal is Dog {
return animal instanceof Dog;
}现在:
if (isDog(animal)) {
animal.bark(); // ✅ 成功!类型被缩小为 Dog
}📌 答案:通过 arg is Type 返回类型,我们告诉 TS:“这个函数的返回值为 true 时,参数 arg 的类型就是 Type”。
学习目标详解
目标一:掌握条件类型语法:T extends U ? X : Y
基本语法
type TypeName<T> =
T extends string ? 'string' :
T extends number ? 'number' :
T extends boolean ? 'boolean' :
'object';
type A = TypeName<'hello'>; // 'string'
type B = TypeName<true>; // 'boolean'
type C = TypeName<{}>; // 'object'分布式条件类型
当 T 是联合类型时,条件类型会自动“分发”到每个成员:
type Wrapped<T> = T extends any ? [T] : never;
type Result = Wrapped<string | number>;
// 相当于 (string extends any ? [string] : never) | (number extends any ? [number] : never)
// 结果:[string] | [number]⚠️ 使用
never可过滤联合类型:
type FilterString<T> = T extends string ? T : never;
type StringsOnly = FilterString<'a' | 1 | 'b' | 2>; // 'a' | 'b'目标二:实现类型过滤与分发
1. 过滤联合类型
type ExtractStrings<T> = T extends string ? T : never;
type T1 = ExtractStrings<'a' | 1 | 'b' | boolean>; // 'a' | 'b'2. 分发式映射
type ToArray<T> = T extends any ? T[] : never;
type T2 = ToArray<string | number>; // string[] | number[]📌 实战:实现 Extract 和 Exclude
// 内置工具类型
type MyExtract<T, U> = T extends U ? T : never;
type MyExclude<T, U> = T extends U ? never : T;
type T3 = MyExtract<'a' | 'b' | 'c', 'a' | 'b'>; // 'a' | 'b'
type T4 = MyExclude<'a' | 'b' | 'c', 'a'>; // 'b' | 'c'目标三:使用 is 和 asserts 编写自定义类型守卫
1. arg is Type(布尔式守卫)
interface Admin {
role: 'admin';
permissions: string[];
}
interface User {
role: 'user';
department: string;
}
type Person = Admin | User;
function isAdmin(person: Person): person is Admin {
return person.role === 'admin';
}
function manage(person: Person) {
if (isAdmin(person)) {
console.log(person.permissions); // ✅ 类型为 Admin
}
}2. asserts condition(断言式守卫)
更强大的方式:直接断言某个条件成立。
function assertIsAdmin(person: Person): asserts person is Admin {
if (person.role !== 'admin') {
throw new Error('Not an admin');
}
}
function manageStrict(person: Person) {
assertIsAdmin(person);
console.log(person.permissions); // ✅ 此后 person 一定是 Admin
}🔍
asserts守卫在函数执行后,整个作用域内都保证类型成立。
目标四:理解内置类型守卫
TS 能识别以下表达式作为类型守卫:
| 守卫 | 作用 | 示例 |
|---|---|---|
typeof v === "string" | 检查原始类型 | typeof x === 'number' |
instanceof C | 检查类实例 | value instanceof Date |
'key' in obj | 检查属性存在 | 'bark' in animal |
Array.isArray() | 检查数组 | Array.isArray(arr) |
| 真值检查 | if (value) { ... } | if (str) { str.trim() } |
📌 真值守卫示例:
function printLength(text: string | null | undefined) {
if (text) {
console.log(text.length); // ✅ text: string(非 null/undefined)
}
}总结:本节核心要点
| 概念 | 关键点 |
|---|---|
| 条件类型 | T extends U ? X : Y,实现类型“if”逻辑 |
| 分布式 | 联合类型自动分发,可用于过滤 |
is 守卫 | arg is Type,用于函数返回值 |
asserts 守卫 | 断言条件成立,影响后续所有代码 |
| 内置守卫 | typeof, instanceof, in, Array.isArray 等 |
📌 下一节我们将学习“映射类型进阶”与“模板字面量类型”,解锁更强大的类型编程能力。
练习题 1:实现条件类型
实现一个类型 GetReturnType<T>,如果 T 是函数,则返回其返回值类型,否则返回 never。
type GetReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
type T1 = GetReturnType<() => string>; // string
type T2 = GetReturnType<string>; // never提示:使用
infer推断返回值类型。
练习题 2:过滤联合类型
实现一个类型 NonNullable<T>,从联合类型中移除 null 和 undefined。
type NonNullable<T> = T extends null | undefined ? never : T;
type T3 = NonNullable<string \| number \| null \| undefined>; // string | number对比内置的
NonNullable<T>。
练习题 3:自定义类型守卫
为以下联合类型编写 in 操作符守卫:
type Shape =
| { kind: 'circle'; radius: number }
| { kind: 'square'; side: number };
function isCircle(shape: Shape): shape is Shape & { kind: 'circle' } {
return shape.kind === 'circle';
}
// 测试
if (isCircle(shape)) {
console.log(shape.radius); // ✅ 应该能访问 radius
}练习题 4:asserts 守卫实战
实现一个函数 assertDefined,断言值不为 null 或 undefined:
function assertDefined<T>(value: T): asserts value is NonNullable<T> {
if (value == null) {
throw new Error('Value is null or undefined');
}
}
// 测试
let maybeStr: string | null = localStorage.getItem('key');
assertDefined(maybeStr);
console.log(maybeStr.length); // ✅ 应该能访问 length练习题 5:内置守卫应用
以下代码能否通过编译?为什么?
function processValue(value: string | number | null) {
if (typeof value === 'string' && value) {
console.log(value.toUpperCase()); // ✅ 可以
}
if (value) {
if (typeof value === 'string') {
console.log(value.toUpperCase()); // ✅ 可以
}
}
}