Oasis's Cloud

一个人的首要责任,就是要有雄心。雄心是一种高尚的激情,它可以采取多种合理的形式。
—— 《一个数学家的辩白》

基本类型

作者:oasis


空集的表示

在前文中我们说类型是一种集合,对于集合来说,会存在空集的概念。例如在函数返回值中,我们可能需要对空集合进行表达。那么怎么定义空集合这一概念呢?

在 TypeScript 中空集合的概念可以借助关键字 never 表示。never 告诉我们一个函数不应该有返回值:

function raise(message: string): never {
    throw new Error(`Error "${message}" raised at ${new Date()}`)
}

大多数编程语言使用 void 表示不存在有意义的值,如果将上面例子中的 never 改为 void,存在误导性。never 表示根本不返回,而 void 表示正常返回,但没有返回值。

function raise(message: string): never;   // 永远不会正常返回
function raise(message: string): void;    // 正常返回,但没有返回值

void 在 TypeScript 中叫做单元类型,单元类型是只有一个可能值的类型,对于这种类型来说,校验其值是没有意义的。所以当函数的结果没有意义,我们使用 void 单元类型。

自定义 Unit 类型

虽然 TypeScript 内置了 void 作为单元类型,但有时我们需要自定义一个更明确的 Unit 类型。实现 Unit 类型有多种方式:

方式一:使用 unique symbol + 私有构造函数

这种方式通过 unique symbol 确保类型唯一性,并通过私有构造函数保证只能创建一个实例:

declare const UnitType: unique symbol;

class Unit {
    [UnitType]: void;
    static readonly value: Unit = new Unit();
    private constructor() { };
}

function greet(): Unit {
    console.log("Hello world!");
    return Unit.value;
}

unique symbol 属性确保相似形状的类型不会被解释为 Unit,私有构造函数确保其他代码不能实例化这个类型,静态只读属性是唯一能够创建的 Unit 实例。

方式二:直接使用 symbol 值

更简单的方式是直接使用一个 symbol 值作为 Unit 类型的唯一值:

const Unit = Symbol('Unit');
type Unit = typeof Unit;

function greet(): Unit {
    console.log("Hello world!");
    return Unit;
}

这种方式更简洁,通过 typeof 操作符从值推导出类型。函数总是返回同一个 symbol 值,等效于返回 void。注意这里 Unit 既是值(symbol 常量)又是类型(通过 typeof 推导)。

方式三:使用字面量类型

也可以使用一个字面量类型来实现 Unit:

type Unit = typeof undefined;  // 或者 typeof null

function greet(): Unit {
    console.log("Hello world!");
    return undefined;
}

不过这种方式语义上不如前两种清晰,因为 undefinednull 在 TypeScript 中有特定的含义。

逻辑和短路

declare 关键字

declare 关键字用于告知编译器”实体已经存在,不用在编译输出中生成它的代码”。declare 用于类型声明而非实现声明。

declare 主要在如下场景中使用:声明全局变量、声明第三方库、扩展已有类型定义、声明环境模块。

在进行前后端衔接的时候,服务端可能会在 html 中加入一些全局变量,在 ts 文件直接使用会报错。

declare const myGlobalVariable: string

在开发过程中,可能需要给 window 对象增加一些属性或方法,这时需要通过 declarewindow 对象类型进行扩展:

declare global {
    interface Window {
        customProperty: string
    }
}

当项目中通过 CDN 或引入无类型的第三方库,可以通过 declare 对第三方库进行类型定义:

declare module "untyped-library" {
    export function doSomething(input: string): number
    export const version: string
}
import {doSomething, version} from 'untyped-library'

在 webpack 等构建工具中,支持引入图片、样式等非 JS 文件。这时候直接引入会出现错误:

declare module "*.png" {
    const content: any
    export default content
}
declare module "*.css" {
    const styles: { [className: string]: string };
    export default styles;
}

declare 声明的类型,通常情况下放到 global.d.ts 文件中,然后在 tsconfig.json 中进行配置:

{
    "compilerOptions": {
        "typeRoots": ["./global.d.ts", "./node_modules"]
    }
}

typeRoots 用于指定类型查找的目录,types 选项指定加载哪些类型,粒度控制更细。

当两个配置项都不包含时,加载如下:

node_modules/
├── @types/
│   ├── node/      # 自动包含 ✓
│   ├── react/     # 自动包含 ✓
│   └── jest/      # 自动包含 ✓
├── lodash/        # 如果有自己的类型,也会包含
└── my-package/
    └── index.d.ts # 自动包含 ✓

设置 typeRoots 后:

{
  "compilerOptions": {
    "typeRoots": ["./custom-types", "./node_modules/@types"]
  }
}

查找顺序变为:

  1. ./custom-types/ 目录下的所有包
  2. ./node_modules/@types/ 目录下的所有包
  3. 不再自动查找其他位置

设置 types 后:

{
    "compilerOptions": {
        "types": ["node", "jest"]
    }
}

同时设置 typeRoots 和 types 后:

{
  "compilerOptions": {
    "typeRoots": ["./custom-types"],
    "types": ["my-custom-types", "node"]
  }
}
  1. 只在 ./custom-types 中查找类型
  2. 只加载 my-custom-typesnode 这两个包的类型
  3. 完全忽略 ./node_modules/@types