理解 TypeScript 的静态类型

这篇博文快速介绍 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;
  ···
}
复制代码

如果你认为这很神秘,那么我同意你的看法。 但是(正如我希望证明的),这个标注相对来说容易学习。 一旦你理解了它,它会告诉你,一个直接,精确和全面的总结这些代码行为。 无需阅读英文长篇描述。

尝试一下代码示例

TypeScript 有一个在线编译。 为了获得最全面的检查,应该打开 “option” 菜单中的所有内容。 这相当于在 --strict 模式下运行TypeScript编译器。

设置全部的类型检查

我总是使用 TypeScript 以最全面的设置 --strict。 没有它,程序的编写会变得相对容易些,但是你也会失去静态类型检查的许多好处。 目前,此设置启用以下子设置:

  • --noImplicitAny: 如果 TypeScript 无法推断出某种类型,说明你没有启用这个选项。 这主要适用于函数和方法的参数:使用此设置,你就必须标注它们的类型。
  • --noImplicitThis: 如果这个类型不明确,提示警告。
  • --alwaysStrict: 尽可能使用 JavaScript 严格模式。
  • --strictNullChecks: null 不是任何类型的一部分(除了它自己的类型,null),并且如果它是可接受的值,则必须明确给值。
  • --strictFunctionTypes: 强化函数类型检查。
  • --strictPropertyInitialization: 如果某个属性的值未定义,那么它必须在构造函数中初始化。

更多信息:TypeScript手册中的“编译器选项”一章。

类型

在这篇博文中,类型是一组值。 JavaScript 语言(不是TypeScript!)有7种类型:

  • Undefined: 元素为未定义的集合。
  • Null: 元素为 null 的集合。
  • Boolean: 元素为 false 或 true 的集合。
  • Number: 元素为数字的集合。
  • String: 元素为字符串的集合。
  • Symbol: 元素为 symbols 的集合。
  • Object: 元素为对象(包括函数和数组)的集合.

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

TypeScript 在 JavaScript 基础上增加了一层:静态类型。它只存在于编译的时候或者源代码类型检查的时候。每一个有静态类型的储存(变量或者属性)地方都可以预知它的值。类型检查确保实现类型推断,不用运行代码就能进行静态类型检查。举个例子,如果函数f(x)的参数x具有静态数字类型,则函数调用f('abc')是非法的,因为参数'abc'是错误的静态类型参数。

类型标注

类型标注在变量的冒号后面。冒号后面的静态类型标注描述了这个变量可以有什么值。下面一个例子表示这个变量只能储存数字类型。

let x: number;
复制代码

你可能想知道如果 x 没有被初始化的时候是不是可以通过静态类型检查。对于这个问题, TypeScript 在给它赋值之前不会让你读取 x 。

类型推断

在 TypeScript 中,即使所有变量你都写静态类型,但你不需要全部明确写它的静态类型。 TypeScript 经常可以推断出它。 例如,如果你写

let x = 123;
复制代码

然后TypeScript推断x具有数字静态类型。

描述类型

类型表达式在类型标注的冒号之后出现。 这些范围从简单到复杂,创建如下

合法的类型表达式有基本类型:

  • JavaScript 动态类型的静态类型:undefined,null,boolean,number,string,symbol,object
  • TypeScript 特定类型:any(所有值的类型)等

注意,“未定义为值”和“未定义为类型”都被定义为未定义。 根据你使用它的地方,它作为一个值或者一个类型。 null也是一样。

可以通过类型运算符组合基本类型来创建更多类型表达式,类型运算符与运算符 union(∪) 和 intersection(∩) 组合类似。

接下来解释 TypeScript 提供的一些类型运算符。

数组作为列表

有两种方法可以表示 Array arr 其元素都是数字的列表:

let arr: number[] = [];
let arr: Array<number> = [];
复制代码

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

稍后,讲解尖括号表示法(Array)。

数组作为元组

如果您在数组中存储两个点,那么你将该数组用作元组。 这看起来如下:

let point: [number, number] = [7, 5];
复制代码

在这种情况下,不需要类型注释。 元组的另一个例子是 Object.entries(obj) 的结果:对于 obj 的每个属性都有一个 [key,value] 对的数组。

> Object.entries({a:1, b:2})
[ [ 'a', 1 ], [ 'b', 2 ] ]
复制代码

Object.entries() 的类型是:

Array<[string, any]>
复制代码

函数类型

这是一个函数类型的例子:

(num: number) => string
复制代码

该类型接受单个数字类型参数,和返回字符串类型的值。 让我们在类型注释中使用这个类型(字符串在这里用作函数):

const func: (num: number) => string = String;
复制代码

同样,我们通常不会在这里使用类型注释,因为 TypeScript 知道 String 的类型,因此可以推断出 func 的类型。 下面的代码是一个更实用的例子:

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

我们使用函数类型来描述 stringify123() 的回调函数。 由于此类型注释,TypeScript 拒绝以下函数调用。

f(String);
复制代码

但它接受以下函数调用:

f(Number);
复制代码

(原文在这可能顺序写反了)

函数结果类型声明

标注函数的所有参数类型是一种很好的实践。 你也可以指定结果类型(但 TypeScript 很适合推断它):

function stringify123(callback: (num: number) => string): string {
  const num = 123;
  return callback(num);
}
复制代码

特殊结果类型 void

void 是函数结果的特殊类型:它告诉 TypeScript 函数总是返回 undefined (显式或隐式):

function f1(): void { return undefined } // OK
function f2(): void { } // OK
function f3(): void { return 'abc' } // error
复制代码

可选参数

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

function stringify123(callback?: (num: number) => string) {
  const num = 123;
  if (callback) {
    return callback(num); // (A)
  }
  return String(num);
}
复制代码

如果您在 --strict 模式下运行 TypeScript ,则会在检查回调没有被省略的情况下让你在 A 行中进行函数调用。

参数默认值

TypeScript 支持 ES6参数默认值

function createPoint(x=0, y=0) {
  return [x, y];
}
复制代码

默认值使参数可选。 通常可以省略类型注释,因为 TypeScript 可以推断类型。 例如,它可以推断x和y都具有数字类型。

如果你想添加类型注释,那看起来如下。

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

剩余类型

您还可以使用 ES6 rest 运算符来处理 TypeScript 参数定义。 相应参数的类型必须是Array:

function joinNumbers(...nums: number[]): string {
    return nums.join('-');
}
joinNumbers(1, 2, 3); // '1-2-3'
复制代码

联合类型

在JavaScript中,变量常常是几种类型之一。 要描述这些变量,你可以使用联合类型。 例如,在以下代码中,x 的类型为 null 或类型 number :

let x = null;
x = 123;
复制代码

x 的类型可以被描述为 null | number :

let x: null|number = null;
x = 123;
复制代码

类型表达式 s | t 的结果是 类型s 和 t 的集合论联合(正如我们前面所看到的那样,这两个集合)。

让我们重写函数 stringify123() :这一次,我们不希望参数回调是可选的。 应该总是传递该参数。 如果调用者不想提供函数,他们必须显式传递null。 这是实现如下:

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

注意,实际上我们必须检查回调是否是一个函数(A行),然后才能在B行进行函数调用。如果没有检查,TypeScript将报告错误。

? 与 undefined| T

类型T的可选参数? 和类型为 undefined | T 的参数非常相似。 (顺便说一句,对于可选属性也是如此。)

主要区别是可以省略可选参数:

function f1(x?: number) { }
f1(); // OK
f1(undefined); // OK
f1(123); // OK
复制代码

但是你不能省略类型为 undefined| T 的参数:

function f1(x?: number) { }
f1(); // OK
f1(undefined); // OK
f1(123); // OK
复制代码

在类型中,通常不包含 null 和 undefined 值

在许多编程语言中, null 是所有类型的一部分。 例如,在 java 中,只要参数的类型是 String ,就可以传递 null 并且 Java 不会报错

相比之下,在 TypeScript 中,undefined 和 null 由不同的类型处理。 如果你想跟上述一样,你需要一个类型联合,比如 undefined | number 和 null | number 。

对象类型

与数组类似,对象在 JavaScript 中扮演两个角色( 偶尔混合 和 或更动态 ):

  • 记录:开发时已知的固定数量的属性。 每个属性可以有不同的类型。
  • 字典:在开发时不知道名称的任意数量的属性。 所有属性键(字符串和/或符号)具有相同的类型,属性值也是如此。

我们将在此文中忽略对象作为词典。 另外,无论如何,Maps 对于字典来说通常是更好的选择。

通过 interfaces 描述对象类型作为记录

interfaces 描述对象类型。 例如:

interface Point {
  x: number;
  y: number;
}
复制代码

TypeScript的类型系统的一大优点是它是结构化的,而不是字面化。 也就是说, interface 匹配具有适当结构的所有对象:

function pointToString(p: Point) {
  return `(${p.x}, ${p.y})`;
}
pointToString({x: 5, y: 7}); // '(5, 7)'
复制代码

相比之下,Java的类型系统需要类来实现接口。

可选属性

如果一个属性可以被省略,你在变量后面加一个问号:

interface Person {
  name: string;
  company?: string;
}
复制代码

函数

Interfaces 也可包含函数:

interface Point {
  x: number;
  y: number;
  distance(other: Point): number;
}
复制代码

类型变量和泛型类型

使用静态类型,有两个级别:

  • 对象级别
  • 元类型级别

相似于

  • 对象储存正常变量
  • 描述类型的变量,也是值类型的变量

正常变量通过 const,let 等引入。 类型变量通过尖括号(<>)引入。 例如,以下代码包含通过引入的类型变量T.

interface Stack<T> {
  push(x: T): void;
  pop(): T;
}
复制代码

你可以看到类型 参数T 在 Stack中 出现两次。 所以这个界面可以直观地理解如下:

  • Stack 是一堆所有具有给定 类型T 的值。每当提到 Stack 时,您必须填写T. 接下来我们将看到如何。
  • push 函数 接受 T类型参数
  • pop 函数 返回 T类型值

如果你使用 Stack ,你就需要给定 T的值。 以下代码显示了一个虚接口 Stack ,其唯一目的是匹配接口。

const dummyStack: Stack<number> = {
  push(x: number) {},
  pop() { return 123 },
};
复制代码

示例:Maps

TypeScript 定义 Map 类型。例如:

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

泛型函数

函数(和方法)也可以引入类型变量:

function id<T>(x: T): T {
  return x;
}
复制代码

使用如下

id<number>(123);
复制代码

由于类型推理,可以省略类型参数

id(123);
复制代码

类型参数的传递

function fillArray<T>(len: number, elem: T) {
  return new Array<T>(len).fill(elem);
}
复制代码

不必显式指定 Array 的类型T - 它是从参数 elem 推断出来的:

const arr = fillArray(3, '*');
  // Inferred type: string[]
复制代码

结论

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

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

这是一个数组的 interface ,其元素的类型为 T ,我们必须在使用此 interface 时填写它们:

方法 .concat() 有零个或更多参数(通过剩余参数操作符)。他们中的每个函数有类型 T[] 或 T 。因此,它可能是类型T 的数组或者是单个 T 的值。

方法 .reduce() 引进了它自己的类型变量,U 。 U 表示以下实体全都具有相同类型(不需要指定,它会自动推断):

  • 回调函数里的 state 参数
  • 回调函数里的结果
  • 方法 .reduce() 的可选参数 firstState
  • .reduce() 的结果

回调函数也获取一个参数 element ,其类型与数组元素的 类型T 相同,参数 index 是数字,参数 array 是 T值 。

进一步阅读

  • 书 (在线免费阅读): “Exploring ES6
  • ECMAScript Language Types” 在 ECMAScript 规范.
  • TypeScript Handbook”: 一本写的很好解释了 TypeScript 支持的多种类型及类型操作。
  • TypeScript 仓库里有完整 ECMAScript 标准库的类型定义。练习类型标注的简单方法是阅读它们。

本文翻译自原文 - Understanding TypeScript’s type notation -> star

由于本人水平有限,错误之处在所难免,敬请指正!

转载请注明出处,保留原文链接以及作者信息。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值