一文学懂 TypeScript 的类型

翻译:疯狂的技术宅
原文: http://2ality.com/2018/04/typ...

本文首发微信公众号:jingchengyideng
欢迎关注,天天都给你推送新鲜的前端技术文章html


你将学到什么

阅读本文后,你应该可以理解如下代码的含义:前端

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

若是你认为这段代码很是神秘 —— 那么我赞成你的意见。可是(我但愿证实)这些符号仍是相对容易学习的。一旦你能理解它们,就能立刻全面、精确的理解这种代码,从而无需再去阅读冗长的英文说明。git

运行代码案例

TypeScript 有一个在线运行环境。为了获得最全面的信息,你应该在 “Options” 菜单中打开全部选项开关。这至关于在 --strict 模式下运行TypeScript编译器。程序员

关于类型检查的详细说明

我在用 TypeScript 时老是喜欢打开 --strict 开关设置。没有它,程序可能会稍微好写一点,可是你也失去了静态类型检查的好处。目前此设置可以开启如下子设置:es6

  • --noImplicitAny:若是 TypeScript 没法推断类型,则必须指定它。这主要用于函数和方法的参数:使用此设置,你必须对它们进行注释。github

    • --noImplicitThis:若是 this 的类型不清楚则会给出提示信息。
    • --alwaysStrict:尽量使用 JavaScript 的严格模式。
    • --strictNullChecksnull 不属于任何类型(除了它本身的类型,null),若是它是可接受的值,则必须明确指定。
    • --strictFunctionTypes:对函数类型更加严格的检查。
    • --strictPropertyInitialization:若是属性的值不能是 undefined ,那么它必须在构造函数中进行初始化。

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

类型

在本文中,咱们把类型看做是一组值的集合。 JavaScript 语言(不是TypeScript!)有7种类型:typescript

  • Undefined:具备惟一元素 undefined 的集合。
  • Null:具备惟一元素“null”的集合。
  • Boolean:具备两个元素 falsetrue 的集合。
  • Number:全部数字的集合。
  • String:全部字符串的集合。
  • Symbol:全部符号的集合。
  • Object:全部对象的集合(包括函数和数组)。

全部这些类型都是 dynamic:能够用在运行时。编程

TypeScript 为 JavaScript 带来了额外的层:静态类型。这些仅在编译或类型检查源代码时存在。每一个存储位置(变量或属性)都有一个静态类型,用于预测其动态值。类型检查可确保这些预测可以实现。还有不少能够进行 静态 检查(不运行代码)的东西。例如,若是函数 f(x) 的参数 x 是静态类型 number,则函数调用 f('abc') 是非法的,由于参数 'abc' 是错误的静态类型。segmentfault

类型注释

变量名后的冒号开始 类型注释:冒号后的类型签名用来描述变量能够接受的值。例如以代码告诉 TypeScript 变量 “x” 只能存储数字:

let x: number;

你可能想知道用 undefined 去初始化 x 是否是违反了静态类型。 TypeScript 不会容许这种状况出现,由于在为它赋值以前不容许操做 x

类型推断

即便在 TypeScript 中每一个存储位置都有静态类型,你也没必要老是明确的去指定它。 TypeScript 一般能够对它的类型进行推断。例如若是你写下这行代码:

let x = 123;

而后 TypeScript 会推断出 x 的静态类型是 number

类型描述

在类型注释的冒号后面出现的是所谓的类型表达式。这些范围从简单到复杂,并按以下方式建立。

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

  • 对应 JavaScript 动态类型的静态类型:

    - `undefined`, `null`
    - `boolean`, `number`, `string`
    - `symbol`
    - `object`
    • 注意:值 undefined 与类型 undefined(取决于所在的位置)
  • TypeScript 的特定类型:

    • Array(从技术上讲不是 JS 中的类型)
    • any(全部值的类型)
    • 等等其余类型

请注意,“undefined做为值“ 和 ”undefined做为类型” 都写作 undefined。根据你使用它的位置,被解释为值或类型。 null 也是如此。

你能够经过类型运算符对基本类型进行组合的方式来建立更多的类型表达式,这有点像使用运算符 union)和intersection()去合并集合。

下面介绍 TypeScript 提供的一些类型运算符。

数组类型

数组在 JavaScript 中扮演如下两个角色(有时是二者的混合):

  • 列表:全部元素都具备相同的类型。数组的长度各不相同。
  • 元组:数组的长度是固定的。元素不必定具备相同的类型。

数组做为列表

数组 arr 被用做列表有两种方法表示 ,其元素都是数字:

let arr: number[] = [];
let arr: Array<number> = [];

一般若是存在赋值的话,TypeScript 就能够推断变量的类型。在这种状况下,实际上你必须帮它解决类型问题,由于在使用空数组时,它没法肯定元素的类型。

稍后咱们将回到尖括号表示法(Array<number>)。

数组做为元组

若是你想在数组中存储二维坐标点,那么就能够把这个数组看成元组去用。看上去是这个样子:

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

在这种状况下,你不须要类型注释。

另一个例子是 Object.entries(obj) 的返回值:一个带有一个 [key,value] 对的数组,它用于描述 obj 的每一个属性。

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

Object.entries() 的返回值类型是:

Array<[string, any]>

函数类型

如下是函数类型的例子:

(num: number) => string

这个类型是一个函数,它接受一个数字类型参数而且返回值为字符串。在类型注释中使用这种类型(String 在这里是个函数)的例子:

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

一样,咱们通常不会在这里使用类型注释,由于 TypeScript 知道 String 的类型,所以能够推断出 func 的类型。

如下代码是一个更实际的例子:

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

因为咱们使用了函数类型来描述 stringify123() 的参数 callback,因此TypeScript 拒绝如下函数调用。

f(Number);

但它接受如下函数调用:

f(String);

函数声明的返回类型

对函数的全部参数进行注释是一个很好的作法。你还能够指定返回值类型(不过 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 时,若是事先检查时发现 callback 没有被省略,它只容许你在 A 行进行函数调用。

参数默认值

TypeScript支持 ES6 参数默认值

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

默认值可使参数可选。一般能够省略类型注释,由于 TypeScript 能够推断类型。例如它能够推断出 xy 都是 number 类型。

若是要添加类型注释,应该这样写:

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

rest 类型

你还能够用 ES6 rest operator 进行 TypeScript 参数定义。相应参数的类型必须是数组:

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

Union

在JavaScript中,有时候变量会是有几种类型之中的一种。要描述这些变量,可使用 union types。例如,在下面的代码中,xnull 类型或 number 类型:

let x = null;
x = 123;

x 的类型能够描述为 null | number

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

类型表达式 s | t 的结果是类型 st 在集合理论意义上的联合(正如咱们以前看到的那样,两个集合)。

下面让咱们重写函数 stringify123():此次咱们不但愿参数 callback 是可选的。应该老是调用它。若是调用者不想传入一个函数,则必须显式传递 null。实现以下。

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

请注意,在行 B 进行函数调用以前,咱们必须再次检查 callback 是否真的是一个函数(行A)。若是没有检查,TypeScript 将会报告错误。

Optional 与 undefined|T

类型为 T 的可选参数和类型为 undefined|T 的参数很是类似。 (另外对于可选属性也是如此。)

主要区别在于你能够省略可选参数:

function f1(x?: number) { }
f1(); // OK
f1(undefined); // OK
f1(123); // OK

But you can’t omit parameters of type
可是你不能省略 undefined|T 类型的参数:

function f2(x: undefined | number) { }
f2(); // error
f2(undefined); // OK
f2(123); // OK

nullundefined 一般不包含在类型中

在许多编程语言中,null 是全部类型的一部分。例如只要 Java 中的参数类型为 String,就能够传递 null 而Java 不会报错。

相反,在TypeScript中,undefinednull 由单独的不相交类型处理。若是你想使它们生效,必需要有一个类型联合,如 undefined|stringnull|string

对象

与Arrays相似,对象在 JavaScript 中扮演两个角色(偶尔混合和/或更加动态):

  • 记录:在开发时已知的固定数量的属性。每一个属性能够有不一样的类型。
  • 字典:在开发时名称未知的任意数量的属性。全部属性键(字符串和/或符号)都具备相同的类型,属性值也是如此。

咱们将在本文章中忽略 object-as-dictionaries。顺便说一句,不管如何,map 一般是比字典的更好选择。

经过接口描述 objects-as-records

接口描述 objects-as-records 。例如:

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

TypeScript 类型系统的一大优点在于它的结构上,而不是在命名上。也就是说,接口 Point 可以匹配适当结构的全部对象:

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

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

可选属性

若是能够省略属性,则在其名称后面加上一个问号:

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

方法

接口内还能够包含方法:

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

类型变量和泛型类型

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

  • 值存在于对象级别
  • 类型存在于元级别

同理:

  • 普通变量定义在对象级别之上。
  • 类型变量存在于元级别之上。它们是值为类型的变量。

普通变量经过 constlet 等引入。类型变量经过尖括号( <> )引入。例如如下代码包含类型变量 T,经过 <T> 引入。

interface Stack<T> {
  push(x: T): void;
  pop(): T;
}

你能够看到类型参数 TStack 的主体内出现两次。所以,该接口能够直观地理解以下:

  • Stack 是一堆值,它们都具备给定的类型 T。每当你提到 Stack 时,必须写 T。接下来咱们会看到究竟该怎么用。
  • 方法 .push() 接受类型为 T 的值。
  • 方法 .pop() 返回类型为 T 的值。

若是使用 Stack,则必须为 T 指定一个类型。如下代码显示了一个虚拟栈,其惟一目的是匹配接口。

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

例子:map

map 在 TypeScript 中的定义。例如:

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);
}

类型变量 T 在这段代码中出现三次:

  • fillArray<T>:引入类型变量
  • elem:T:使用类型变量,从参数中选择它。
  • Array<T>:将 T 传递给 Array 的构造函数。

这意味着:咱们没必要显式指定Array<T>的类型 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;
  ···
}

这是一个Array的接口,其元素类型为 T,每当使用这个接口时必须填写它:

  • 方法.concat()有零个或多个参数(经过 rest 运算符定义)。其中每个参数中都具备类型 T[]|T。也就是说,它是一个 T 类型的数组或是一个 T 值。
  • 方法.reduce() 引入了本身的类型变量 UU 表示如下实体都具备相同的类型(你不须要指定,它是自动推断的):

    • Parameter state of callback() (which is a function)
    • statecallback() 的参数(这是一个函数)

      • Result of callback()
    • callback()的返回
    • .reduce()的可选参数 firstState

      • Result of .reduce()
    • .reduce()的返回

callback 还将得到一个 element 参数,其类型与 Array 元素具备相同的类型 T,参数 index 是一个数字,参数 arrayT 的值。

扩展阅读


  • 本文首发微信公众号:jingchengyideng

    欢迎扫描二维码关注公众号,天天都给你推送新鲜的前端技术文章

    欢迎扫描二维码关注公众号,天天都给你推送新鲜的前端技术文章



    欢迎继续阅读本专栏其它高赞文章: