程序员scholar 程序员scholar
首页
  • Java 基础

    • JavaSE
    • JavaIO
    • JavaAPI速查
  • Java 高级

    • JUC
    • JVM
    • Java新特性
    • 设计模式
  • Web 开发

    • Servlet
    • Java网络编程
  • Web 标准

    • HTML
    • CSS
    • JavaScript
  • 前端框架

    • Vue2
    • Vue3
    • Vue3 + TS
    • 微信小程序
    • uni-app
  • 工具与库

    • jQuery
    • Ajax
    • Axios
    • Webpack
    • Vuex
    • WebSocket
    • 第三方登录
  • 后端与语言扩展

    • ES6
    • Typescript
    • node.js
  • Element-UI
  • Apache ECharts
  • 数据结构
  • HTTP协议
  • HTTPS协议
  • 计算机网络
  • Linux常用命令
  • Windows常用命令
  • SQL数据库

    • MySQL
    • MySQL速查
  • NoSQL数据库

    • Redis
    • ElasticSearch
  • 数据库

    • MyBatis
    • MyBatis-Plus
  • 消息中间件

    • RabbitMQ
  • 服务器

    • Nginx
  • Spring框架

    • Spring6
    • SpringMVC
    • SpringBoot
    • SpringSecurity
  • SpringCould微服务

    • SpringCloud基础
    • 微服务之DDD架构思想
  • 日常必备

    • 开发常用工具包
    • Hutoll工具包
    • IDEA常用配置
    • 开发笔记
    • 日常记录
    • 项目部署
    • 网站导航
    • 产品学习
    • 英语学习
  • 代码管理

    • Maven
    • Git教程
    • Git小乌龟教程
  • 运维工具

    • Docker
    • Jenkins
    • Kubernetes
  • 算法笔记

    • 算法思想
    • 刷题笔记
  • 面试问题常见

    • 十大经典排序算法
    • 面试常见问题集锦
关于
GitHub (opens new window)
首页
  • Java 基础

    • JavaSE
    • JavaIO
    • JavaAPI速查
  • Java 高级

    • JUC
    • JVM
    • Java新特性
    • 设计模式
  • Web 开发

    • Servlet
    • Java网络编程
  • Web 标准

    • HTML
    • CSS
    • JavaScript
  • 前端框架

    • Vue2
    • Vue3
    • Vue3 + TS
    • 微信小程序
    • uni-app
  • 工具与库

    • jQuery
    • Ajax
    • Axios
    • Webpack
    • Vuex
    • WebSocket
    • 第三方登录
  • 后端与语言扩展

    • ES6
    • Typescript
    • node.js
  • Element-UI
  • Apache ECharts
  • 数据结构
  • HTTP协议
  • HTTPS协议
  • 计算机网络
  • Linux常用命令
  • Windows常用命令
  • SQL数据库

    • MySQL
    • MySQL速查
  • NoSQL数据库

    • Redis
    • ElasticSearch
  • 数据库

    • MyBatis
    • MyBatis-Plus
  • 消息中间件

    • RabbitMQ
  • 服务器

    • Nginx
  • Spring框架

    • Spring6
    • SpringMVC
    • SpringBoot
    • SpringSecurity
  • SpringCould微服务

    • SpringCloud基础
    • 微服务之DDD架构思想
  • 日常必备

    • 开发常用工具包
    • Hutoll工具包
    • IDEA常用配置
    • 开发笔记
    • 日常记录
    • 项目部署
    • 网站导航
    • 产品学习
    • 英语学习
  • 代码管理

    • Maven
    • Git教程
    • Git小乌龟教程
  • 运维工具

    • Docker
    • Jenkins
    • Kubernetes
  • 算法笔记

    • 算法思想
    • 刷题笔记
  • 面试问题常见

    • 十大经典排序算法
    • 面试常见问题集锦
关于
GitHub (opens new window)
npm

(进入注册为作者充电)

  • TypeScript

    • TypeScript - 介绍
    • TypeScript - 安装和使用
    • TypeScript - 基本类型
    • TypeScript - 编译和配置
    • TypeScript - 文件打包
    • TypeScript - 接口
    • TypeScript - 函数
    • TypeScript - 类
    • TypeScript - 泛型
    • TypeScript 的导入导出
    • TypeScript - 类型推断
    • TypeScript - 高级类型
      • 1. 交叉类型
      • 2. 联合类型
      • 3. 类型保护
        • 用户自定义的类型保护
        • typeof 类型保护
        • instanceof 类型保护
      • 4. 可以为 null 的类型
        • 可选参数和可选属性
        • 类型保护和类型断言
      • 5. 字符串字面量类型
      • 6. 总结
  • JS 超集语言 - TypeScript
  • TypeScript
scholar
2023-09-08
目录

TypeScript - 高级类型

  • 1. 交叉类型
  • 2. 联合类型
  • 3. 类型保护
    • 用户自定义的类型保护
    • typeof 类型保护
    • instanceof 类型保护
  • 4. 可以为 null 的类型
    • 可选参数和可选属性
    • 类型保护和类型断言
  • 5. 字符串字面量类型
  • 6. 总结

# 1. 交叉类型

交叉类型(Intersection Types)是将多个类型合并为一个类型。这种方式可以将多个类型的成员合并到一个类型中,使得新类型同时具备所有类型的特性。交叉类型通常用于混入(mixins)或其他不符合传统面向对象模型的场景。

示例:

function extend<T, U>(first: T, second: U): T & U {
    let result = {} as T & U;
    for (let id in first) {
        result[id] = first[id] as any;
    }
    for (let id in second) {
        if (!result.hasOwnProperty(id)) {
            result[id] = second[id] as any;
        }
    }
    return result;
}

class Person {
    constructor(public name: string) {}
}

interface Loggable {
    log(): void;
}

class ConsoleLogger implements Loggable {
    log() {
        console.log('Logging...');
    }
}

let jim = extend(new Person('Jim'), new ConsoleLogger());
let n = jim.name;
jim.log();
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

在这个例子中,extend 函数将 Person 和 ConsoleLogger 合并成一个新的对象,该对象同时具有 Person 和 Loggable 的成员。这说明 jim 变量既可以访问 name 属性,也可以调用 log 方法。

# 2. 联合类型

联合类型(Union Types)与交叉类型类似,但其使用方式不同。联合类型表示一个值可以是几种类型之一。在需要一个值可以是多种类型之一的情况下,使用联合类型是很有用的。

示例:

function padLeft(value: string, padding: string | number) {
    if (typeof padding === 'number') {
        return Array(padding + 1).join(' ') + value;
    }
    if (typeof padding === 'string') {
        return padding + value;
    }
    throw new Error(`Expected string or number, got '${padding}'.`);
}

let indentedString = padLeft('Hello world', 4); // 返回 "    Hello world"
1
2
3
4
5
6
7
8
9
10
11

在这个例子中,padLeft 函数的 padding 参数可以是 string 或 number 类型。通过使用联合类型,我们可以限制 padding 只能是 string 或 number,避免传入其他类型导致的错误。

联合类型表示一个值可以是几种类型之一。我们使用竖线(|)分隔每个类型,例如 number | string 表示一个值可以是 number 或 string。

当一个值是联合类型时,我们只能访问这个联合类型中所有类型共有的成员。

示例:

interface Bird {
    fly(): void;
    layEggs(): void;
}

interface Fish {
    swim(): void;
    layEggs(): void;
}

function getSmallPet(): Fish | Bird {
    // 实现省略
    return {} as Fish; // 示例返回一个 Fish 类型
}

let pet = getSmallPet();
pet.layEggs(); // 正常
pet.swim();    // 错误:类型 'Bird | Fish' 上不存在属性 'swim'。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

在这个例子中,getSmallPet 函数返回一个 Fish 或 Bird 类型的对象。pet 变量是 Fish | Bird 类型,我们可以调用 layEggs 方法,因为它是 Fish 和 Bird 的公共成员。但不能调用 swim 方法,因为 Bird 类型没有 swim 方法。

联合类型允许我们根据类型的不同来编写更加灵活的代码,同时保持类型的安全性。

# 3. 类型保护

联合类型适合用于那些值可以是不同类型的场景。但有时我们希望确切地了解一个值是 Fish 还是 Bird。在 JavaScript 中,我们可以通过检查对象成员的存在来区分这两种可能的值。在 TypeScript 中,由于只能访问联合类型中共有的成员,因此我们需要使用类型保护来确保特定的类型。

let pet = getSmallPet();

// 每一个成员访问都会报错,因为 TypeScript 无法确定 pet 的具体类型
if (pet.swim) {
    pet.swim();
} else if (pet.fly) {
    pet.fly();
}
1
2
3
4
5
6
7
8

为了让这段代码正常工作,我们需要使用类型断言,告诉 TypeScript 编辑器该对象是某种特定的类型:

let pet = getSmallPet();

// 使用类型断言,将 pet 视为 Fish 类型
if ((pet as Fish).swim) {
    (pet as Fish).swim();
} else {
    // 使用类型断言,将 pet 视为 Bird 类型
    (pet as Bird).fly();
}
1
2
3
4
5
6
7
8
9

# 用户自定义的类型保护

在上面的代码中,我们需要多次使用类型断言。如果我们能够在检查过类型后,在每个分支中清楚地知道 pet 的类型,那会更加简洁。TypeScript 的类型保护机制让这成为可能。类型保护是一种运行时检查,以确保某个范围内的变量属于特定类型。定义一个类型保护,我们可以简单地定义一个返回值为类型谓词的函数:

// 定义类型保护函数,判断 pet 是否为 Fish 类型
function isFish(pet: Fish | Bird): pet is Fish {
    return (pet as Fish).swim !== undefined;
}
1
2
3
4

在这个例子中,pet is Fish 就是一个类型谓词。类型谓词的形式为 parameterName is Type,其中 parameterName 必须是当前函数签名中的一个参数名。

每当使用一个变量调用 isFish 时,TypeScript 会将变量缩小为那个具体的类型。

if (isFish(pet)) {
    // 在此分支中,pet 被缩小为 Fish 类型
    pet.swim();
} else {
    // 在此分支中,pet 被缩小为 Bird 类型
    pet.fly();
}
1
2
3
4
5
6
7

TypeScript 不仅知道在 if 分支中 pet 是 Fish 类型,它还知道在 else 分支中 pet 一定不是 Fish 类型,而是 Bird 类型。

# typeof 类型保护

现在我们回头看看如何使用联合类型来重写 padLeft 函数。我们可以像下面这样利用类型断言:

// 定义类型保护函数,判断 x 是否为 number 类型
function isNumber(x: any): x is number {
    return typeof x === 'number';
}

// 定义类型保护函数,判断 x 是否为 string 类型
function isString(x: any): x is string {
    return typeof x === 'string';
}

// 使用类型保护函数来实现类型检查
function padLeft(value: string, padding: string | number) {
    if (isNumber(padding)) {
        return Array(padding + 1).join(' ') + value;
    }
    if (isString(padding)) {
        return padding + value;
    }
    throw new Error(`Expected string or number, got '${padding}'.`);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

然而,我们不需要将 typeof x === 'number' 抽象成一个函数,因为 TypeScript 可以将它识别为类型保护。这意味着我们可以直接在代码中检查类型:

function padLeft(value: string, padding: string | number) {
    if (typeof padding === 'number') {
        // padding 是 number 类型
        return Array(padding + 1).join(' ') + value;
    }
    if (typeof padding === 'string') {
        // padding 是 string 类型
        return padding + value;
    }
    throw new Error(`Expected string or number, got '${padding}'.`);
}
1
2
3
4
5
6
7
8
9
10
11

这些 typeof 类型保护仅限于两种形式:typeof v === "typename" 和 typeof v !== "typename"。其中 typename 必须是 "number","string","boolean" 或 "symbol"。虽然 TypeScript 不会阻止你与其他字符串进行比较,但它不会将这些表达式识别为类型保护。

# instanceof 类型保护

如果你已经了解了 typeof 类型保护,并且对 JavaScript 中的 instanceof 操作符熟悉,你可能已经猜到我们要讨论的内容。

instanceof 类型保护是通过构造函数来缩小类型的一种方式。我们对之前的例子做一个小小的改动:

// 定义 Bird 类
class Bird {
    fly() {
        console.log('bird fly');
    }

    layEggs() {
        console.log('bird lay eggs');
    }
}

// 定义 Fish 类
class Fish {
    swim() {
        console.log('fish swim');
    }

    layEggs() {
        console.log('fish lay eggs');
    }
}

// 随机返回 Bird 或 Fish 的实例
function getRandomPet() {
    return Math.random() > 0.5 ? new Bird() : new Fish();
}

let pet = getRandomPet();

// 使用 instanceof 检查 pet 是否为 Bird 类型
if (pet instanceof Bird) {
    pet.fly();
}
// 使用 instanceof 检查 pet 是否为 Fish 类型
if (pet instanceof Fish) {
    pet.swim();
}
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

instanceof 用于检查自定义类,而 typeof 用于检查内置基本类型。通过使用这些类型保护机制,我们可以在代码中更安全地处理联合类型。

Sure! Here is the revised content with detailed comments in the code blocks:

# 4. 可以为 null 的类型

TypeScript 具有两种特殊的类型,null 和 undefined,它们分别具有值 null 和 undefined。我们在 TypeScript - 基础类型 中已经做过简要说明。默认情况下,类型检查器认为 null 与 undefined 可以赋值给任何类型。null 与 undefined 是所有其它类型的一个有效值。这也意味着,即使你想阻止将它们赋值给其它类型也不行。null 的发明者,Tony Hoare,称它为 价值亿万美金的错误 (opens new window)。

使用 --strictNullChecks 标记可以解决此错误:当你声明一个变量时,它不会自动地包含 null 或 undefined。你可以使用联合类型明确地包含它们:

let s: string = 'foo'; // 定义一个字符串类型的变量 s
s = null; // 错误, 'null' 不能赋值给 'string'
let sn: string | null = 'bar'; // 定义一个可以为字符串或 null 的变量 sn
sn = null; // 可以

sn = undefined; // 错误, 'undefined' 不能赋值给 'string | null'
1
2
3
4
5
6

注意,按照 JavaScript 的语义,TypeScript 会把 null 和 undefined 区别对待。string | null,string | undefined 和 string | undefined | null 是不同的类型。

# 可选参数和可选属性

使用了 --strictNullChecks,可选参数会被自动地加上 | undefined:

function f(x: number, y?: number) {
    // y 是一个可选参数,自动被加上 | undefined
    return x + (y || 0);
}
f(1, 2); // 正常调用,y 被赋值为 2
f(1); // 正常调用,y 未赋值,默认为 undefined
f(1, undefined); // 正常调用,y 被显式赋值为 undefined
f(1, null); // 错误, 'null' 不能赋值给 'number | undefined'
1
2
3
4
5
6
7
8

可选属性也会有同样的处理:

class C {
    a: number; // 必须初始化的属性
    b?: number; // 可选属性,自动被加上 | undefined
}
let c = new C();
c.a = 12; // 正常赋值
c.a = undefined; // 错误, 'undefined' 不能赋值给 'number'
c.b = 13; // 正常赋值
c.b = undefined; // 可以赋值为 undefined
c.b = null; // 错误, 'null' 不能赋值给 'number | undefined'
1
2
3
4
5
6
7
8
9
10

# 类型保护和类型断言

由于可以为 null 的类型可以和其它类型定义为联合类型,你需要使用类型保护来去除 null。幸运的是,这与在 JavaScript 里写的代码一致:

function f(sn: string | null): string {
    if (sn === null) {
        // 如果 sn 为 null,返回默认字符串
        return 'default';
    } else {
        // 如果 sn 不为 null,返回 sn
        return sn;
    }
}
1
2
3
4
5
6
7
8
9

这里显式地去除了 null,你也可以使用短路运算符:

function f(sn: string | null): string {
    // 如果 sn 为 null,返回 'default',否则返回 sn
    return sn || 'default';
}
1
2
3
4

如果编译器不能够去除 null 或 undefined,你可以使用类型断言手动去除。语法是添加 ! 后缀:identifier! 从 identifier 的类型里去除了 null 和 undefined。

! 的作用就是告诉 TypeScript 编辑器:该属性一定存在值,不可能是 null 或者 undefined。

function broken(name: string | null): string {
    function postfix(epithet: string) {
        // 这里编译器报错,'name' 可能为 null
        return name.charAt(0) + '.  the ' + epithet;
    }
    name = name || 'Bob'; // 如果 name 为 null,赋值为 'Bob'
    return postfix('great');
}

function fixed(name: string | null): string {
    function postfix(epithet: string) {
        // 使用 ! 去除 name 的 null 类型
        return name!.charAt(0) + '.  the ' + epithet;
    }
    name = name || 'Bob'; // 如果 name 为 null,赋值为 'Bob'
    return postfix('great');
}

broken(null); // 调用 broken 函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

本例使用了嵌套函数,因为编译器无法去除嵌套函数的 null(除非是立即调用的函数表达式)。因为它无法跟踪所有对嵌套函数的调用,尤其是你将内层函数做为外层函数的返回值。如果无法知道函数在哪里被调用,就无法知道调用时 name 的类型。

# 5. 字符串字面量类型

字符串字面量类型允许你指定字符串必须具有的确切值。在实际应用中,字符串字面量类型可以与联合类型和类型保护很好地配合。通过结合使用这些特性,你可以实现类似枚举类型的字符串。

type Easing = 'ease-in' | 'ease-out' | 'ease-in-out'; // 定义字符串字面量类型

class UIElement {
    animate(dx: number, dy: number, easing: Easing) {
        if (easing === 'ease-in') {
            // ...
        } else if (easing === 'ease-out') {
            // ...
        } else if (easing === 'ease-in-out') {
            // ...
        } else {
            // error! 不能传入 null 或者 undefined.
        }
    }
}

let button = new UIElement();
button.animate(0, 0, 'ease-in'); // 正确调用,传入了允许的字符串字面量
button.animate(0, 0, 'uneasy'); // 错误,传入了不允许的字符串字面量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

你只能从三种允许的字符中选择其一来作为参数传递,传入其它值则会产生错误。

Argument of type '"uneasy"' is not assignable to parameter of type '"ease-in" | "ease-out" | "ease-in-out"'
1

# 6. 总结

到这里,我们的 TypeScript 常用语法学习就告一段落了。当然,TypeScript 还有其他的语法我们并没有讲到。我们只是讲了 TypeScript 的一些常用语法,学会这些知识已经足以开发一般的应用了。如果在使用 TypeScript 开发项目中遇到其他的 TypeScript 语法知识,你可以通过 TypeScript 的 官网文档 (opens new window) 学习。因为学基础最好的方法还是去阅读它的官网文档,并敲上面的小例子。要记住学习一门技术的基础,官网文档永远是最好的第一手资料。

然而,TypeScript 的学习不能仅仅靠看官网文档,你还需要动手实践。在实践中你才能真正掌握 TypeScript。

编辑此页 (opens new window)
上次更新: 2024/12/28, 18:32:08
TypeScript - 类型推断

← TypeScript - 类型推断

Theme by Vdoing | Copyright © 2019-2025 程序员scholar
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式