掌握 TypeScript
请支持本书:购买捐赠
(广告,请勿屏蔽。)

7 TypeScript 要点



本章解释了 TypeScript 的要点。

7.1 您将学到什么

阅读本章后,您应该能够理解以下 TypeScript 代码

interface Array<T> {
  concat(...items: Array<T[] | T>): T[];
  reduce<U>(
    callback: (state: U, element: T, index: number, array: T[]) => U,
    firstState?: U
  ): U;
  // ···
}

您可能认为这很神秘。我同意你的看法!但是(我希望证明)这种语法相对容易学习。一旦您理解了它,它就会为您提供代码行为的即时、精确和全面的摘要,而无需阅读冗长的英文描述。

7.2 指定类型检查的全面性

可以通过多种方式配置 TypeScript 编译器。一组重要的选项控制编译器检查 TypeScript 代码的彻底程度。最大设置通过 `--strict` 激活,我建议始终使用它。这使得程序稍微难以编写,但我们也获得了静态类型检查的全部好处。

  这就是您现在需要了解的有关 `--strict` 的全部内容

如果您想了解更多详细信息,请继续阅读。

将 `--strict` 设置为 `true` 会将以下所有选项设置为 `true`

在本书的后面,当我们开始使用 TypeScript 创建 npm 包Web 应用程序 时,我们将看到更多编译器选项。TypeScript 手册有关于它们的全面文档

7.3 TypeScript 中的类型

在本章中,类型只是一组值。JavaScript 语言(不是 TypeScript!)只有八种类型

  1. Undefined:唯一元素为 `undefined` 的集合
  2. Null:唯一元素为 `null` 的集合
  3. Boolean:包含两个元素 `false` 和 `true` 的集合
  4. Number:所有数字的集合
  5. BigInt:所有任意精度整数的集合
  6. String:所有字符串的集合
  7. Symbol:所有符号的集合
  8. Object:所有对象的集合(包括函数和数组)

所有这些类型都是*动态的*:我们可以在运行时使用它们。

TypeScript 为 JavaScript 带来了一个额外的层:*静态类型*。这些仅在编译或类型检查源代码时存在。每个存储位置(变量、属性等)都有一个静态类型,用于预测其动态值。类型检查可确保这些预测成真。

并且有很多东西可以*静态地*检查(无需运行代码)。例如,如果函数 `toString(num)` 的参数 `num` 具有静态类型 `number`,则函数调用 `toString('abc')` 是非法的,因为参数 `'abc'` 具有错误的静态类型。

7.4 类型注解

function toString(num: number): string {
  return String(num);
}

前面的函数声明中有两个类型注解

`number` 和 `string` 都是*类型表达式*,用于指定存储位置的类型。

7.5 类型推断

通常,如果没有类型注解,TypeScript 可以*推断*静态类型。例如,如果我们省略 `toString()` 的返回类型,TypeScript 会推断它是 `string`

// %inferred-type: (num: number) => string
function toString(num: number) {
  return String(num);
}

类型推断不是猜测:它遵循明确的规则(类似于算术)来推导未明确指定的类型。在这种情况下,return 语句将函数 `String()`(将任意值映射到字符串)应用于类型为 `number` 的值 `num` 并返回结果。这就是推断的返回类型为 `string` 的原因。

如果既未明确指定也无法推断位置的类型,则 TypeScript 会对其使用类型 `any`。这是所有值的类型和通配符,因为如果值具有该类型,我们可以做任何事情。

使用 `--strict` 时,仅当我们显式使用 `any` 时才允许使用它。换句话说:每个位置都必须具有显式或推断的静态类型。在以下示例中,参数 `num` 两者都没有,并且我们收到编译时错误

// @ts-expect-error: Parameter 'num' implicitly has an 'any' type. (7006)
function toString(num) {
  return String(num);
}

7.6 通过类型表达式指定类型

类型注解冒号后的类型表达式从简单到复杂,创建如下。

基本类型是有效的类型表达式

有很多方法可以组合基本类型来产生新的*复合类型*。例如,通过*类型运算符*,它们组合类型的方式类似于集合运算符*并集* (`∪`) 和*交集* (`∩`) 组合集合的方式。我们很快就会看到如何做到这一点。

7.7 两种语言级别:动态与静态

TypeScript 有两种语言级别

我们可以在语法中看到这两个级别

const undef: undefined = undefined;

请注意,相同的语法 `undefined` 意味着不同的东西,具体取决于它是在动态级别还是静态级别使用。

  尝试培养对两种语言级别的意识

这非常有助于理解 TypeScript。

7.8 类型别名

使用 `type` 我们可以为现有类型创建一个新名称(别名)

type Age = number;
const age: Age = 82;

7.9 数组类型

数组在 JavaScript 中扮演两个角色(一个或两个)

7.9.1 数组作为列表

有两种方法可以表示数组 `arr` 被用作其元素都是数字的列表

let arr1: number[] = [];
let arr2: Array<number> = [];

通常,如果有赋值,TypeScript 可以推断变量的类型。在这种情况下,我们实际上必须帮助它,因为对于空数组,它无法确定元素的类型。

我们稍后会回到尖括号表示法 (`Array<number>`)。

7.9.2 数组作为元组

如果我们将二维点存储在数组中,那么我们将该数组用作元组。如下所示

let point: [number, number] = [7, 5];

数组作为元组需要类型注解,因为对于数组字面量,TypeScript 推断列表类型,而不是元组类型

// %inferred-type: number[]
let point = [7, 5];

`Object.entries(obj)` 的结果是元组的另一个示例:一个数组,其中包含 `obj` 的每个属性的一个 [键,值] 对。

// %inferred-type: [string, number][]
const entries = Object.entries({ a: 1, b: 2 });

assert.deepEqual(
  entries,
  [[ 'a', 1 ], [ 'b', 2 ]]);

推断的类型是一个元组数组。

7.10 函数类型

这是一个函数类型的示例

(num: number) => string

此类型包含接受单个数字类型参数并返回字符串的每个函数。让我们在类型注解中使用此类型

const toString: (num: number) => string = // (A)
  (num: number) => String(num); // (B)

通常,我们必须为函数指定参数类型。但在这种情况下,可以从 A 行的函数类型推断出 B 行中 `num` 的类型,我们可以省略它

const toString: (num: number) => string =
  (num) => String(num);

如果我们省略 `toString` 的类型注解,TypeScript 会从箭头函数推断出一个类型

// %inferred-type: (num: number) => string
const toString = (num: number) => String(num);

这一次,`num` 必须有一个类型注解。

7.10.1 更复杂的示例

以下示例更复杂

function stringify123(callback: (num: number) => string) {
  return callback(123);
}

我们正在使用函数类型来描述 `stringify123()` 的参数 `callback`。由于此类型注解,TypeScript 拒绝以下函数调用。

// @ts-expect-error: Argument of type 'NumberConstructor' is not
// assignable to parameter of type '(num: number) => string'.
//   Type 'number' is not assignable to type 'string'.(2345)
stringify123(Number);

但它接受此函数调用

assert.equal(
  stringify123(String), '123');

7.10.2 函数声明的返回类型

TypeScript 通常可以推断函数的返回类型,但允许显式指定它们,并且偶尔有用(至少,它没有任何坏处)。

对于 `stringify123()`,指定返回类型是可选的,如下所示

function stringify123(callback: (num: number) => string): string {
  return callback(123);
}
7.10.2.1 特殊返回类型 `void`

`void` 是函数的特殊返回类型:它告诉 TypeScript 函数始终返回 `undefined`。

它可以显式地这样做

function f1(): void {
  return undefined;
}

或者它可以隐式地这样做

function f2(): void {}

但是,这样的函数不能显式返回 `undefined` 以外的值

function f3(): void {
  // @ts-expect-error: Type '"abc"' is not assignable to type 'void'. (2322)
  return 'abc';
}

7.10.3 可选参数

标识符后的问号表示该参数是可选的。例如

function stringify123(callback?: (num: number) => string) {
  if (callback === undefined) {
    callback = String;
  }
  return callback(123); // (A)
}

只有在我们确保 `callback` 不是 `undefined`(如果省略了参数,则为 `undefined`)的情况下,TypeScript 才允许我们进行 A 行中的函数调用。

7.10.3.1 参数默认值

TypeScript 支持 参数默认值

function createPoint(x=0, y=0): [number, number] {
  return [x, y];
}

assert.deepEqual(
  createPoint(),
  [0, 0]);
assert.deepEqual(
  createPoint(1, 2),
  [1, 2]);

默认值使参数可选。我们通常可以省略类型注释,因为 TypeScript 可以推断类型。例如,它可以推断出 xy 都具有 number 类型。

如果我们想添加类型注释,则如下所示。

function createPoint(x:number = 0, y:number = 0): [number, number] {
  return [x, y];
}

7.10.4 剩余参数

我们也可以在 TypeScript 参数定义中使用 剩余参数。它们的静态类型必须是数组(列表或元组)

function joinNumbers(...nums: number[]): string {
  return nums.join('-');
}
assert.equal(
  joinNumbers(1, 2, 3),
  '1-2-3');

7.11 联合类型

变量持有的值(一次一个值)可能是不同类型的成员。在这种情况下,我们需要一个联合类型。例如,在以下代码中,stringOrNumberstring 类型或 number 类型

function getScore(stringOrNumber: string|number): number {
  if (typeof stringOrNumber === 'string'
    && /^\*{1,5}$/.test(stringOrNumber)) {
      return stringOrNumber.length;
  } else if (typeof stringOrNumber === 'number'
    && stringOrNumber >= 1 && stringOrNumber <= 5) {
    return stringOrNumber
  } else {
    throw new Error('Illegal value: ' + JSON.stringify(stringOrNumber));
  }
}

assert.equal(getScore('*****'), 5);
assert.equal(getScore(3), 3);

stringOrNumber 的类型为 string|number。类型表达式 s|t 的结果是类型 st 的集合论并集(解释为集合)。

7.11.1 默认情况下,类型中不包含 undefinednull

在许多编程语言中,null 是所有对象类型的一部分。例如,每当 Java 中变量的类型为 String 时,我们都可以将其设置为 null,并且 Java 不会报错。

相反,在 TypeScript 中,undefinednull 由单独的、不相交的类型处理。如果我们想允许它们,则需要联合类型,例如 undefined|stringnull|string

let maybeNumber: null|number = null;
maybeNumber = 123;

否则,我们会收到错误

// @ts-expect-error: Type 'null' is not assignable to type 'number'. (2322)
let maybeNumber: number = null;
maybeNumber = 123;

请注意,TypeScript 不会强制我们立即初始化(只要我们不在初始化之前从变量中读取数据)。

let myNumber: number; // OK
myNumber = 123;

7.11.2 使省略显式

回想一下之前提到的这个函数

function stringify123(callback?: (num: number) => string) {
  if (callback === undefined) {
    callback = String;
  }
  return callback(123); // (A)
}

让我们重写 stringify123(),以便参数 callback 不再是可选的:如果调用者不想提供函数,则必须显式传递 null。结果如下所示。

function stringify123(
  callback: null | ((num: number) => string)) {
  const num = 123;
  if (callback === null) { // (A)
    callback = String;
  }
  return callback(num); // (B)
}

assert.equal(
  stringify123(null),
  '123');

// @ts-expect-error: Expected 1 arguments, but got 0. (2554)
assert.throws(() => stringify123());

同样,我们必须先处理 callback 不是函数的情况(A 行),然后才能在 B 行进行函数调用。如果我们没有这样做,TypeScript 会在该行报告错误。

7.12 可选参数与默认值与 undefined|T

以下三个参数声明非常相似

如果参数可选,则可以省略。在这种情况下,它的值为 undefined

function f1(x?: number) { return x }

assert.equal(f1(123), 123); // OK
assert.equal(f1(undefined), undefined); // OK
assert.equal(f1(), undefined); // can omit

如果参数具有默认值,则在省略参数或将其设置为 undefined 时将使用该值

function f2(x = 456) { return x }

assert.equal(f2(123), 123); // OK
assert.equal(f2(undefined), 456); // OK
assert.equal(f2(), 456); // can omit

如果参数具有联合类型,则不能省略,但我们可以将其设置为 undefined

function f3(x: undefined | number) { return x }

assert.equal(f3(123), 123); // OK
assert.equal(f3(undefined), undefined); // OK

// @ts-expect-error: Expected 1 arguments, but got 0. (2554)
f3(); // can’t omit

7.13 类型化对象

与数组类似,对象在 JavaScript 中扮演着两个角色(有时会混合使用)

我们在本章中忽略了作为字典的对象——它们在 §15.4.5 “索引签名:作为字典的对象” 中介绍。顺便说一句,地图通常是字典的更好选择。

7.13.1 通过接口对作为记录的对象进行类型化

接口描述作为记录的对象。例如

interface Point {
  x: number;
  y: number;
}

我们也可以用逗号分隔成员

interface Point {
  x: number,
  y: number,
}

7.13.2 TypeScript 的结构类型化与名义类型化

TypeScript 类型系统的一大优势是它以结构化方式工作,而不是以名义化方式工作。也就是说,接口 Point 匹配具有适当结构的所有对象

interface Point {
  x: number;
  y: number;
}
function pointToString(pt: Point) {
  return `(${pt.x}, ${pt.y})`;
}

assert.equal(
  pointToString({x: 5, y: 7}), // compatible structure
  '(5, 7)');

相反,在 Java 的名义类型系统中,我们必须为每个类显式声明它实现的接口。因此,一个类只能实现其创建时存在的接口。

7.13.3 对象字面量类型

对象字面量类型是匿名接口

type Point = {
  x: number;
  y: number;
};

对象字面量类型的一个好处是可以内联使用它们

function pointToString(pt: {x: number, y: number}) {
  return `(${pt.x}, ${pt.y})`;
}

7.13.4 可选属性

如果可以省略属性,我们在其名称后放一个问号

interface Person {
  name: string;
  company?: string;
}

在以下示例中,johnjane 都匹配接口 Person

const john: Person = {
  name: 'John',
};
const jane: Person = {
  name: 'Jane',
  company: 'Massive Dynamic',
};

7.13.5 方法

接口也可以包含方法

interface Point {
  x: number;
  y: number;
  distance(other: Point): number;
}

就 TypeScript 的类型系统而言,方法定义和值为函数的属性是等效的

interface HasMethodDef {
  simpleMethod(flag: boolean): void;
}
interface HasFuncProp {
  simpleMethod: (flag: boolean) => void;
}

const objWithMethod: HasMethodDef = {
  simpleMethod(flag: boolean): void {},
};
const objWithMethod2: HasFuncProp = objWithMethod;

const objWithOrdinaryFunction: HasMethodDef = {
  simpleMethod: function (flag: boolean): void {},
};
const objWithOrdinaryFunction2: HasFuncProp = objWithOrdinaryFunction;

const objWithArrowFunction: HasMethodDef = {
  simpleMethod: (flag: boolean): void => {},
};
const objWithArrowFunction2: HasFuncProp = objWithArrowFunction;

我的建议是使用最能表达如何设置属性的语法。

7.14 类型变量和泛型

回想一下 TypeScript 的两种语言级别

类似地

  命名类型参数

在 TypeScript 中,通常对类型参数使用单个大写字母(例如 TIO)。但是,允许使用任何合法的 JavaScript 标识符,并且更长的名称通常使代码更易于理解。

7.14.1 示例:值的容器

// Factory for types
interface ValueContainer<Value> {
  value: Value;
}

// Creating one type
type StringContainer = ValueContainer<string>;

Value 是一个类型变量。可以在尖括号之间引入一个或多个类型变量。

7.15 示例:泛型类

类也可以具有类型参数

class SimpleStack<Elem> {
  #data: Array<Elem> = [];
  push(x: Elem): void {
    this.#data.push(x);
  }
  pop(): Elem {
    const result = this.#data.pop();
    if (result === undefined) {
        throw new Error();
    }
    return result;
  }
  get length() {
    return this.#data.length;
  }
}

SimpleStack 具有类型参数 Elem。当我们实例化类时,我们还为类型参数提供一个值

const stringStack = new SimpleStack<string>();
stringStack.push('first');
stringStack.push('second');
assert.equal(stringStack.length, 2);
assert.equal(stringStack.pop(), 'second');

7.15.1 示例:映射

映射在 TypeScript 中是泛型类型化的。例如

const myMap: Map<boolean,string> = new Map([
  [false, 'no'],
  [true, 'yes'],
]);

由于类型推断(基于 new Map() 的参数),我们可以省略类型参数

// %inferred-type: Map<boolean, string>
const myMap = new Map([
  [false, 'no'],
  [true, 'yes'],
]);

7.15.2 函数和方法的类型变量

函数定义可以像这样引入类型变量

function identity<Arg>(arg: Arg): Arg {
  return arg;
}

我们按如下方式使用该函数。

// %inferred-type: number
const num1 = identity<number>(123);

由于类型推断,我们可以再次省略类型参数

// %inferred-type: 123
const num2 = identity(123);

请注意,TypeScript 推断出类型 123,它是一个包含一个数字的集合,比类型 number 更具体。

7.15.2.1 箭头函数和方法

箭头函数也可以具有类型参数

const identity = <Arg>(arg: Arg): Arg => arg;

这是方法的类型参数语法

const obj = {
  identity<Arg>(arg: Arg): Arg {
    return arg;
  },
};

7.15.3 更复杂的函数示例

function fillArray<T>(len: number, elem: T): T[] {
  return new Array<T>(len).fill(elem);
}

类型变量 T 在此代码中出现四次

我们在调用 fillArray() 时可以省略类型参数(A 行),因为 TypeScript 可以从参数 elem 推断出 T

// %inferred-type: string[]
const arr1 = fillArray<string>(3, '*');
assert.deepEqual(
  arr1, ['*', '*', '*']);

// %inferred-type: string[]
const arr2 = fillArray(3, '*'); // (A)

7.16 结论:理解初始示例

让我们使用我们所学的知识来理解我们之前看到的代码片段

interface Array<T> {
  concat(...items: Array<T[] | T>): T[];
  reduce<U>(
    callback: (state: U, element: T, index: number, array: T[]) => U,
    firstState?: U
  ): U;
  // ···
}

这是元素类型为 T 的数组的接口