笔记:《深入理解 TypeScript》

Bigno
大约 60 分钟

感谢原版书籍作者 🔗 @Basarat,以及本书中文作者 🔗 @jkchao

原书链接:

所有编译结果均通过 🔗 https://www.typescriptlang.org/play 编译得到,配置如下:

  • tsconfig:playground 默认;

  • version:v4.5.2。

tsconfig.json

关于 tsconfig.json 的文档,见:🔗 https://www.typescriptlang.org/tsconfig

JSON
{
  "compilerOptions": {
    /* 基本选项 */
    "target": "es5",                       // 指定编译的 ECMAScript 目标版本: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', 'ES2021', 'ESNext', 'JSON'
    "module": "commonjs",                  // 指定使用(编译后的)模块: 'None', 'CommonJS', 'AMD', 'UMD', 'System', 'ES2015', 'ES2020', 'ES2022', 'ESNext', 'Node12', 'NodeNext'
    "lib": [],                             // 指定要包含在编译中的库文件
    "allowJs": true,                       // 允许编译 javascript 文件
    "checkJs": true,                       // 报告 javascript 文件中的错误
    "jsx": "preserve",                     // 指定 jsx 代码的生成: 'preserve', 'react-native', or 'react'
    "declaration": true,                   // 生成相应的 '.d.ts' 文件
    "sourceMap": true,                     // 生成相应的 '.map' 文件
    "outFile": "./",                       // 将输出文件合并为一个文件
    "outDir": "./",                        // 指定输出目录
    "rootDir": "./",                       // 用来控制输出目录结构 --outDir.
    "removeComments": true,                // 删除编译后的所有的注释
    "noEmit": true,                        // 不生成输出文件
    "importHelpers": true,                 // 从 tslib 导入辅助工具函数
    "isolatedModules": true,               // 将每个文件作为单独的模块 (与 'ts.transpileModule' 类似).

    /* 严格的类型检查选项 */
    "strict": true,                        // 启用所有严格类型检查选项
    "noImplicitAny": true,                 // 在表达式和声明上有隐含的 any类型时报错
    "strictNullChecks": true,              // 启用严格的 null 检查
    "noImplicitThis": true,                // 当 this 表达式值为 any 类型的时候,生成一个错误
    "alwaysStrict": true,                  // 以严格模式检查每个模块,并在每个文件里加入 'use strict'

    /* 额外的检查 */
    "noUnusedLocals": true,                // 有未使用的变量时,抛出错误
    "noUnusedParameters": true,            // 有未使用的参数时,抛出错误
    "noImplicitReturns": true,             // 并不是所有函数里的代码都有返回值时,抛出错误
    "noFallthroughCasesInSwitch": true,    // 报告 switch 语句的 fallthrough 错误。(即,不允许 switch 的 case 语句贯穿)

    /* 模块解析选项 */
    "moduleResolution": "node",            // 选择模块解析策略: 'node' (Node.js) or 'classic' (TypeScript pre-1.6)
    "baseUrl": "./",                       // 用于解析非相对模块名称的基目录
    "paths": {},                           // 模块名到基于 baseUrl 的路径映射的列表
    "rootDirs": [],                        // 根文件夹列表,其组合内容表示项目运行时的结构内容
    "typeRoots": [],                       // 包含类型声明的文件列表
    "types": [],                           // 需要包含的类型声明文件名列表
    "allowSyntheticDefaultImports": true,  // 允许从没有设置默认导出的模块中默认导入。

    /* Source Map Options */
    "sourceRoot": "./",                    // 指定调试器应该找到 TypeScript 文件而不是源文件的位置
    "mapRoot": "./",                       // 指定调试器应该找到映射文件而不是生成文件的位置
    "inlineSourceMap": true,               // 生成单个 soucemaps 文件,而不是将 sourcemaps 生成不同的文件
    "inlineSources": true,                 // 将代码与 sourcemaps 生成到一个文件中,要求同时设置了 --inlineSourceMap 或 --sourceMap 属性

    /* 其他选项 */
    "experimentalDecorators": true,        // 启用装饰器
    "emitDecoratorMetadata": true          // 为装饰器提供元数据的支持
  }
}

target 属性

指定编译的 ECMAScript 目标版本,下文翻译自:🔗 https://www.typescriptlang.org/tsconfig#target

现代浏览器支持所有 ES6 特性,所以 ES6 是一个不错的选择。如果您的代码部署到较旧的环境,您可以选择设置版本较低的 target,如果您的代码保证在较新的环境中运行,则可以选择设置较高的 target


target 决定了哪些 JS 特性会被降级,哪些保持不变。例如,如果目标是 ES5 或更低版本,箭头函数 () => this 将被转换为等效的函数表达式。


target 也会决定 lib 的默认值。您可以根据需要自行搭配 targetlib 设置,但为了方便起见,可以只设置 target


对于像 Node 这样的开发者平台,target 字段会有个基准,具体取决于平台的类型及其版本。您可以在 🔗 tsconfig/bases 找到一组社区组织的 TSConfig,其中包含常见平台及其版本的配置。


特殊值 ESNext,将会指定 TypeScript 能支持的最高版本。应谨慎使用此设置,因为它在不同的 TypeScript 版本之间可能不尽相同,并且会使升级更难以预测。

JSX 属性

jsx 控制 TypeScript 编译 JSX 的模式,这些模式只会在「代码生成阶段」起作用,且类型检查不受影响。🔗 https://www.typescriptlang.org/tsconfig#jsx

该选项决定 JSX 在 JavaScript 文件中如何被处理,现有如下选项:

  • react:使用 React.createElement 处理 JSX,生成 .js 文件;

  • react-jsx:使用 react/jsx-runtime 处理 JSX,生成 .js 文件;

  • react-dev-jsx:使用 react/jsx-dev-runtime 处理 JSX,生成 .js 文件;

  • react-native:不对 JSX 进行处理,生成 .js 文件;

  • preserve:不对 JSX 进行处理,生成 .jsx 文件;

JSX
export const helloWorld = () => <h1>Hello world</h1>;
不同选项编译的产物

关于更多 react/jsx,见🔗 介绍全新的 JSX 转换

简而言之,这是 React 17 后的一个新功能,JSX 语法没变,新增支持 Babel 和 TypeScript 等编译器直接使用 react/jsx-runtime 进行 JSX 的编译,无需引入 React。(无需手动引入,开发者不感知)。

所以单独使用 JSX 的话无需手动引入 React 了(Babel 8+,TypeScript v4.1+)。

  • 泛型组件的使用:

TSX
// 一个泛型组件
type SelectProps<T> = { items: T[] };
class Select<T> extends React.Component<SelectProps<T>, any> {}

// 使用
const Form = () => <Select<string> items={['a', 'b']} />;

  • 泛型函数在 TSX 中的使用(使用 extends):

TSX
// ERROR: const foo = <T>(x: T) => T; // Error: T 标签没有关闭
const foo = <T extends {}>(x: T) => x;

  • 断言在 TSX 中使用(使用 as):

TSX
// ERROR: const foo = <T>bar; // Error: T 标签没有关闭
const foo = bar as T;

lib.d.ts

安装 TypeScript 的时候,会顺带安装所有 lib 文件,都保存在 node_modules/typescript/lib 目录下,这些 lib 在指定使用后,会被包含到编译上下文,对开发起到提示、静态类型检查等作用。

lib 文件可以分为三类:

  • 运行环境:一些基础运行时的类型声明,如 DOM、Worker 等;

  • JavaScript Base:特定版本的 JavaScript 基础接口定义;

  • 功能选项:可以通俗的理解为当前版本 JavaScript 相较于上一个版本「新增的接口」。

当你在 tsconfig.json 中指定了不同的 target,则 TypeScript 会导入不同的 lib 文件,如何导入以及文件如何划分我们稍后来说,先来看看安装完 TypeScript 后,都有哪些 lib 文件。

类型文件分类

看看都有什么文件:🔗 https://github.com/microsoft/TypeScript/tree/main/lib

我们进入到 lib 文件夹下,可以看到包含许多类型声明(配合下方代码食用),其中:

  • lib.[ESVersion].d.ts:如 lib.es2020.d.ts

包含了当前版本所有的「功能选项」,以及上一代版本的 JavaScript Base;

由于每个版本都包含了上个版本的接口定义,换句话说,假设我引用了 esnext,一直到 es5 的所有 JavaScript Base 都会被引入,无需额外指定;

  • lib.[ESVersion].full.d.ts:如 lib.es2020.full.d.ts

从名字上也能看出,该文件包含了对应版本的 JavaScript 所有接口能力,以及「运行环境」;

  • lib.[ESVersion].[OptionalFunc].d.ts:如:lib.es2020.promise.d.ts

文件名也看得出,具体定义了某个特定版本的特定接口类型;

TypeScript
// ...

/// <reference lib="es2019" />
/// <reference lib="es2020.bigint" />
/// <reference lib="es2020.promise" />
/// <reference lib="es2020.sharedmemory" />
/// <reference lib="es2020.string" />
/// <reference lib="es2020.symbol.wellknown" />
/// <reference lib="es2020.intl" />
lib.d.ts

lib 和 target

前面说到tsconfig.json 中指定了不同的 target,则 TypeScript 会导入不同的 lib 文件,那么他们的关系是什么?

首先,我们先看看 lib 属性,其中 TypeScript 官网对 lib 属性的介绍:

下文翻译自:🔗 https://www.typescriptlang.org/tsconfig/#lib

TypeScript 包括一组内置 JS API(如 Math)的默认类型定义,以及浏览器环境(如 document)中的类型定义。

TypeScript 还为新的 JS 功能提供了 API,用于与指定的 target 进行匹配;例如:如果 target 是 ES6 或更高版本,则可使用 Map 的定义。

您可能出于以下几个原因想要更改这些:

  • 您的程序不在浏览器中运行,因此您不需要 dom 类型定义;

  • 您的运行时平台提供了某些 JavaScript API 对象(可能通过 polyfills),但尚不支持给定 ECMAScript 版本的完整语法;

  • 您有一些(但不是全部)更高级别 ECMAScript 版本的 polyfill 或本机实现。

在 TypeScript 4.5 中,lib 文件可以被 npm 模块覆盖,请在🔗 博客中了解更多信息。

接着,我们再来看看 target,我们之前在 target 属性 也介绍了,如果没有指定 lib,TypeScript 会根据不同的 target 会决定不同 lib 值;

这块的代码见:🔗 utilitiesPublic.ts#L13-L36

TypeScript
export function getDefaultLibFileName(options: CompilerOptions): string {
    // 下面根据 Option 中的 target 配置,决定引入哪个 lib
    switch (getEmitScriptTarget(options)) {
        case ScriptTarget.ESNext:
            return "lib.esnext.full.d.ts";
        case ScriptTarget.ES2022:
            return "lib.es2022.full.d.ts";
        case ScriptTarget.ES2021:
            return "lib.es2021.full.d.ts";
        case ScriptTarget.ES2020:
            return "lib.es2020.full.d.ts";
        case ScriptTarget.ES2019:
            return "lib.es2019.full.d.ts";
        case ScriptTarget.ES2018:
            return "lib.es2018.full.d.ts";
        case ScriptTarget.ES2017:
            return "lib.es2017.full.d.ts";
        case ScriptTarget.ES2016:
            return "lib.es2016.full.d.ts";
        case ScriptTarget.ES2015:
            return "lib.es6.d.ts";  // We don't use lib.es2015.full.d.ts due to breaking change.
        default:
            return "lib.d.ts"; // 默认返回 lib.d.ts
    }
}

export function getEmitScriptTarget(compilerOptions: {module?: CompilerOptions["module"], target?: CompilerOptions["target"]}) {
    // 获取配置中的 target
    return compilerOptions.target ||
        (compilerOptions.module === ModuleKind.Node16 && ScriptTarget.ES2022) ||
        (compilerOptions.module === ModuleKind.NodeNext && ScriptTarget.ESNext) ||
        ScriptTarget.ES3;
}

TypeScript 类型系统

一些前置的概念:

  • 🔗 编译上下文:也就是 tsconfig.json,因为 tsc 和 typescript 共用同一份配置(IDE 也会使用这个配置)。

  • 🔗 声明空间:分为「类型声明空间」和「变量声明空间」:

    • 类型声明空间:存放所有类型,用于给变量声明类型,不能用作变量;

    • 变量声明空间:存放所有可用作变量的内容,不能用于类型声明(除非像 Class 会同时在两个空间进行声明)。

  • 🔗 动态查找:指导入模块的时候,没有指定路径,则 TS 会根据配置中的 moduleResolution 选择「模块解析策略」,进行模块的动态查找。

  • 🔗 命名空间namespace 关键字,可用于函数分组(或不需要实例化的 Class)。

  • 类型注解:TypeAnnotation 语法。在「类型声明空间」中可用的任何内容都可以用作类型注解。

  • 别名type 关键字,其生成的就是一个「类型别名」。

命名空间

本质上是创建了一个对象(可以是已经存在的对象),在其中 export 的所有 function 都会被挂载到该对象下;

JavaScript
"use strict";
var Utility;
(function (Utility) {
    function log(msg) {
        console.log(msg);
    }
    Utility.log = log;
    function error(msg) {
        console.log(msg);
    }
    Utility.error = error;
})(Utility || (Utility = {}))
命名空间

@types

默认情况下,所有可见的 @types 包都会被引入到项目,包括下面这些路径

  • ./node_modules/@types/

  • ../node_modules/@types/

  • ../../node_modules/@types/

如果 types 字段被指定,则只有列出的 packages 会被引入到全局范围,如:

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

环境声明

你可以通过 declare 关键字来告诉 TypeScript,你正在试图表述一个其他地方已经存在的代码(原本就存在的代码,如第三方 JS 库,或者 Node 原生 API 和变量)

TypeScript
interface Process {
  exit(code?: number): void;
}

declare let process: Process; // proces 已经存在了,declare 只是为它声明类型

枚举

🔗 一文让你彻底掌握 TS 枚举 这篇写的挺全

  • 普通枚举,可以看到编译前后,第六行几乎没有区别,且运行时存在 Tristate 变量:

TypeScript
enum Tristate {
  False,
  True
}

const lie = Tristate.False;
普通枚举编译

  • 常量枚举,TS 会将枚举的所用用法进行「内联」,运行时并不存在 Tristate 变量:

如果是用「常量枚举」的同事,开启了 tsconfig.json 中的 preserveConstEnums 配置,这运行时会保留 Tristate 变量,但是和常量枚举的生产结果不太一样,见上面第三个 Tab。

TypeScript
const enum Tristate {
  False,
  True
}

const lie = Tristate.False;
常量枚举 —— 性能优化点

  • 枚举 + 命名空间,可以向枚举中添加「静态方法」:

TypeScript
enum Weekday {
  Monday,
  Tuesday,
  Wednesday,
  Thursday,
  Friday,
  Saturday,
  Sunday
}

namespace Weekday {
  export function isBusinessDay(day: Weekday) {
    switch (day) {
      case Weekday.Saturday:
      case Weekday.Sunday:
        return false;
      default:
        return true;
    }
  }
}
enum + namespace 向枚举中添加「静态方法」

上面的原理也非常简单,因为本质上 enumnamaspace 都是使用/创建一个对象,并向该对象上添加内容,所以可以这么做,见上面编译后的代码。

函数重载

TypeScript 中对函数进行重载以应对不同入参情况进行类型检查,只需多次声明函数头,最后一个函数头是在函数体内实际处于活动状态,但不可用于外部

TypeScript
function padding(all: number);
function padding(topAndBottom: number, leftAndRight: number);
function padding(top: number, right: number, bottom: number, left: number);
// Actual implementation that is a true representation of all the cases the function body needs to handle
function padding(a: number, b?: number, c?: number, d?: number) {
  if (b === undefined && c === undefined && d === undefined) {
    b = c = d = a;
  } else if (c === undefined && d === undefined) {
    c = a;
    d = b;
  }
  return {
    top: a,
    right: b,
    bottom: c,
    left: d
  };
}
函数重载

可以看到,重载并不会对实际代码有任何影响。

类型断言与类型转换

类型断言之所以不被称为「类型转换」,是因为转换通常意味着某种运行时的支持。但是,类型断言纯粹是一个编译时语法



当我们想对一个对象进行断言,但是与对象原本的类型不重合的时候,可以使用「双重断言」:

重合:当 S 类型是 T 类型的子集,或者 T 类型是 S 类型的子集时,S 能被成功断言成 T

这是为了在进行类型断言时提供额外的安全性,完全毫无根据的断言是危险的,如果你想这么做,你可以使用 unknown

TypeScript
/* ❌ ERROR */
function handler(event: Event) {
  const mouseEvent = event as HTMLElement;
  // 类型 "Event" 到类型 "HTMLElement" 的转换可能是错误的,因为两种类型不能充分重叠。如果这是有意的,请先将表达式转换为 "unknown"。
  //   类型“Event”缺少类型“HTMLElement”的以下属性: accessKey, accessKeyLabel, autocapitalize, dir 及其他 273 项。ts(2352)
}

/* ✅ RIGHT */
function handler(event: Event) {
  const mouseEvent = event as unknown as HTMLElement;
}

Freshness

也被称作「对象字面量严格检查」(strict object literal checking);

一种对象字面量的检查方式,确保对象字面量在结构上「类型兼容」。

📢
注意

Freshness 只对「对象字面量」起作用,错误提示也只会发生在「对象字面量」上。

之所以只对「对象字面量」进行类型检查,是因为在这种情况下,那些实际没有被使用到的「属性」有可能会拼写错误或者被误用。

TypeScript
function logName(something: { name: string }) {
  console.log(something.name);
}

logName({ name: 'matt' }); // ok
logName({ name: 'matt', job: 'being awesome' }); // Error: 对象字面量只能指定已知属性,`job` 属性在这里并不存在。

这样做的好处就是:我们不能传递函数不需要的参数进去,否则当我们只看到函数调用的时候,由于没有错误提示,误认为 job 也是函数处理所需要的一部分(实际上没有用,只是多传递了参数);

类型保护

类型保护在条件判断的语句中非常使用,可以用来缩小类型的范围:

TypeScript
interface A {
  x: number;
}

interface B {
  y: string;
}

function doStuff(q: A | B) {
  if ('x' in q) {
    // q: A
  } else {
    // q: B
  }
}

此外,还可以使用 is 关键字显示声明函数返回值类型,进一步缩小类型范围:

TypeScript
interface Foo {
  foo: number;
  common: string;
}

interface Bar {
  bar: number;
  common: string;
}

// 用户自己定义的类型保护!
// 相比于返回 Boolean,使用 `is` 可以帮助 TS 缩小类型范围,避免隐蔽的类型错误
// 由于并没有在运行时的自我检查机制,仅仅返回 Boolean 的话,在父级的条件语句中依旧无法得知当前条件块中处理的数据是何种类型
function isFoo(arg: Foo | Bar): arg is Foo {
  return (arg as Foo).foo !== undefined;
}

// 用户自己定义的类型保护使用用例:
function doStuff(arg: Foo | Bar) {
  if (isFoo(arg)) {
    console.log(arg.foo); // ok
    console.log(arg.bar); // Error
  } else {
    console.log(arg.foo); // Error
    console.log(arg.bar); // ok
  }
}

类型兼容性

  • 任何类型都能赋值给 any

  • 枚举和数字类型相互兼容;(但是不同枚举之间就算数值相同也不兼容);

  • 类之间兼容只会检查实例成员和方案是否兼容(构造函数和静态成员不会被检查),privateprotected 成员必须来自相同的类;

  • 协变:子类型(Child)能够赋值给父类型(Base);

  • 入参逆变,返回值协变,见另一篇文章;

Never

never 是 TypeScript 最底层的类型,在分析「代码流」的时候,这会是一个理所当然存在的类型:

  • 「从不会有返回值」(包含 while(true) {} )或者「总是抛出错误」的函数会被推断返回值类型为 never

  • 任何类型(包括 undefinednull)都不能赋值给 never 类型,只有 never 本身能够赋值给 never

  • void 表示没有返回任何类型(返回值为空),never 表示永远不存在返回值的类型(永不返回,如一定抛出错误);

never 有一个用于「检查联合类型」的作用,考虑下面的例子:

TypeScript
interface Square {
  kind: 'square';
  size: number;
}

interface Rectangle {
  kind: 'rectangle';
  width: number;
  height: number;
}

type Shape = Square | Rectangle;

function area(s: Shape) {
  if (s.kind === 'square') {
    return s.size * s.size; // 这时候 `s` 一定是 `Square` 类型
  } else if (s.kind === 'rectangle') {
    return s.width * s.height; // 这时候 `s` 一定是 `Rectangle` 类型
  }
}

如果我们要添加一个新的 Shape 类型 Circle,但是有可能会在 area() 函数中忘记添加对新的类型的处理:

TypeScript
interface Square {
  kind: 'square';
  size: number;
}

interface Rectangle {
  kind: 'rectangle';
  width: number;
  height: number;
}

// 有人仅仅是添加了 `Circle` 类型
interface Circle {
  kind: 'circle';
  radius: number;
}

type Shape = Square | Rectangle | Circle; // 新增 Circle

function area(s: Shape) {
  if (s.kind === 'square') {
    return s.size * s.size;
  } else if (s.kind === 'rectangle') {
    return s.width * s.height;
  }
  // 没有添加对 Circle 类型的处理
}
// 其实如果开启了 `noImplicitReturns: true` 的话
// 上面的 area 会报出 `Not all code paths return a value.(7030)` // 并非所有代码路径都返回值。ts(7030)

所以我们可以对 else 语句进行剩余类型的判断:

TypeScript
interface Square {
  kind: 'square';
  size: number;
}

interface Rectangle {
  kind: 'rectangle';
  width: number;
  height: number;
}

// 有人仅仅是添加了 `Circle` 类型
// 我们可能希望 TypeScript 能在任何被需要的地方抛出错误
interface Circle {
  kind: 'circle';
  radius: number;
}

type Shape = Square | Rectangle | Circle;

function area(s: Shape) {
  if (s.kind === 'square') {
    return s.size * s.size;
  } else if (s.kind === 'rectangle') {
    return s.width * s.height;
  } else {
    // Error: 'Circle' 不能被赋值给 'never'
    const _exhaustiveCheck: never = s;
  }
}

可以看到,正常情况下,else 中的 s 一定是 never 类型,因为所有的联合类型成员都被处理过了;

但是,如果 s 还有可能是其他的类型(如上面的 Circle),是不能赋值给 never,起到了一个可以用来捕获错误的检查

它会强制你添加一个新的条件判断:else if (s.kind === 'circle'),以保证覆盖了所有 path。

索引签名

在 TypeScript 中,对象的索引,必须是 stringnumber 或者 symbol

我们称为「索引签名」,如 obj['foo'],表示一个 string 类型的索引签名;

TypeScript
const foo: {
  [index: string]: { message: string }; // index 也可以为 number
}

// 储存的东西必须符合结构
// ok
foo['a'] = { message: 'some message' };

// Error, 必须包含 `message`
foo['a'] = { messages: 'some message' };

在 JavaScript 中,如果使用「对象」作为索引,默认会调用其 toString() 方法将其转换为 string,然后再进行读取;

在 TypeScript 中,这是不被允许的,原因有两点:

  1. toString() 调用是隐隐式的,开发者可能不知道自己使用的是一个对象;

  2. 对象调用 toString() 在 v8 引擎上总是会返回 [object Object]

当我们声明了一个索引签名,所有明确的「成员的类型」都必须要和「索引类型」相同

如果一定得不同,建议将索引签名下放到具体的某一个属性

TypeScript
// ok
interface Foo {
  [key: string]: number;
  x: number;
  y: number;
}

// Error
interface Bar {
  [key: string]: number;
  x: number;
  y: string; // Error: y 属性必须为 number 类型
}

// 下放索引签名以兼容(更好的设计,而不是同时使用索引签名和有效变量)
interface NestedCSS {
  color?: string;
  nest?: {
    [selector: string]: NestedCSS;
  };
}

// 其实你也可以使用「交叉类型」来进行 Hack,但是创建对象字面量的时候仍然会遇到问题
type FieldState = { value: string };
type FormState = { isValid: boolean } & { [fieldName: string]: FieldState };

// ✅ 正常工作,用于从某些「其他地方」获取的 JavaScript 对象
declare const foo: FormState;

const isValidBool = foo.isValid;
const somethingFieldState = foo['something'];

// ❌ 当使用它来创建一个对象时会报错
const bar: FormState = {
  // 'isValid' 不能赋值给 'FieldState'
  isValid: false
};
// 不能将类型 “{ isValid: false; }” 分配给类型 “FormState”。
//   不能将类型 “{ isValid: false; }” 分配给类型 “{ [fieldName: string]: FieldState; }”。
//     属性 “isValid” 与索引签名不兼容。
//       不能将类型 “boolean” 分配给类型 “FieldState”。ts(2322)

当然,也可以遍历联合类型,来声明索引类型:

TypeScript
type Index = 'a' | 'b' | 'c';
type FromIndex = { [k in Index]?: number }; // 遍历联合类型 Index,每次迭代都赋值给 k
// type FromIndex = {
//     a?: number | undefined;
//     b?: number | undefined;
//     c?: number | undefined;
// }

type FromSomeIndex<K extends string> = { [key in K]: number }; // 使用泛型进行「延迟推断」

📒 总结

  • 枚举:枚举有许多多种类,比如数字枚举、字符串枚举、常量枚举等;

    • 非「常量枚举」在编译后会保存枚举对象,可以直接当作对象使用;

    • 「常量枚举」(const enum)可以作为一个性能优化点:输入不会包含任何定义的枚举变量,并内联所有的枚举值;

    • 注意数字枚举在使用的时候和 number 兼容,注意作为传参时候的兼容,和作为 log 输出时候的信息不明晰;

  • lib:TypeScript 会在安装的时候同时安装所有的 lib 依赖库,会被包含到编译上下文,对开发起到提示、静态类型检查等作用。

    • 默认会根据 target 返回对应的依赖库,ES5 默认导入 lib.d.ts

    • lib 分为运行环境(DOM、Webworker 等)、JavaScript Base(ES5、ES2015、ES2016 等)、功能选项(如 lib.es2015.iterable.d.ts);

    • lib.esX.d.ts 都包含了上一代 JavaScript Base lib.es(X-1).d.ts,和当前版本所有「功能选项」;

    • lib.esX.full.d.ts 都包含 lib.esX.d.ts(也就是上面的内容)和「运行环境」;

    • 往定义好的类型上添加属性,如 window 对象,可以使用 interface 关键字进行声明;

  • 函数重载:TypeScript 支持声明函数重载,对于文档和类型安全很重要;

    • 「函数头」需要在真正的函数前被声明;

    • 真正的函数「不可被调用」,只用于重载的实际调用;

  • 类型断言:断言纯粹是一个编译时语法,表示你比编译器更了解这个类型;

    • <断言类型> 写法在 JSX 中不兼容;

    • 「双重断言」用来断言那些无法进行单个断言的情况,如对参数进行协变;一般用 as unknown as

  • Freshness:对象字面量严格检查,用于检查字面量结构;(是一种能力,下方写法可以同时使用 Freshness,也能放宽 Freshness 的检查)

    • 可以使用「索引签名」用来包含和使用「额外的属性」;

    • 可以使用 ? 可选属性,让某些属性「非必填」;

  • 类型保护:用来告诉 TypeScript 当前(联合类型)对象是某个确定的对象,以提供完整正确的检查;

    • 可以使用 instanceoftypeof 进行「类型判断」;

    • 可以使用 in 进行「特定属性判断」,进而确定具体类型;

    • 可以使用 is 进行「范围缩小」,缩小返回值范围;

  • never:永不返回或者总是抛出错误的函数的返回类型;

    • 只有 never 能够赋值给 never

    • 可以用作 code path 检查是否覆盖所有条件分支;

  • 索引签名:对访问对象成员的「一系列索引」进行的「类型声明」;

    • 索引签名需要和其他「成员的类型」保持一致,如果不一致,建议重新考虑数据的结构,可以将索引签名下放到某一个具体的属性中;

    • 可以通过遍历联合类型进行索引签名的声明。