22

TypeScript,已经成为前端避不开的基础

在读完《深入理解TypeScript》之后,写下这篇总结

TypeScript解决的最关键痛点是什么?

Type类型的约束、不确定情况下的提示、在代码编写阶段就能知道自己的错误

这三点我认为是最关键的点,本身TypeScript能做的事情,JavaScript都能做,虽然使用TS要多写很多代码,但是其实真正算下来,是可以节省大量时间,因为你在编写的时候就能知道哪里有问题。

呼吁大家,全面拥抱TypeScript ,TypeScript肯定是未来

需要从JavaScript项目迁移:

假设:

你知道 JavaScript

你知道在项目中使用常用的方式和构建工具(如:webpack)。

有了以上假设,从 JavaScript 迁移,总的来说包括以下步骤:

添加一个 tsconfig.json文件;

把文件扩展名从 .js 改成 .ts,开始使用 any 来减少错误;

开始在 TypeScript 中写代码,尽可能的减少 any 的使用;

回到旧代码,开始添加类型注解,并修复已识别的错误;

为你的第三方 JavaScript 代码定义环境声明。

记住所有的 JavaScript 都是有效的 TypeScript。这意味着,如果让 TypeScript 编译器编译 TypeScript 里的 JavaScript 代码,编译后的结果将会与原始的 JavaScript 代码一模一样。也就是说,把文件扩展名从 .js 改成 .ts 将不会造成任何负面的影响。

第三方代码

你可以将你的 JavaScript 的代码改成 TypeScript 代码,但是你不能让这个世界都使用 TypeScript。这正是 TypeScript 环境声明支持的地方。我们建议你创建一个 vendor.d.ts 文件作为开始(.d.ts 文件扩展名指定这个文件是一个声明文件),然后我们可以向文件里添加东西。或者,你也可以创建一个针对于特定库的声明文件,如为 jquery 创建 jquery.d.ts 文件。

几乎排名前 90%JavaScript 库的声明文件存在于 DefinitelyTyped 这样一个仓库里,在创建自己定义的声明文件之前,我们建议你先去仓库中寻找。虽然创建一个声明文件这种快速但是不好的方式是减小使用 TypeScript 初始阻力的重要步骤。

考虑使用 jquery 的用例,你可以非常简单快速的为它创建一个定义:

declare var $: any;

有时候,你可能想给某些变量一些明确的定义(如:jquery),并且你会在类型声明空间中使用它。你可以通过 type 关键字快速的实现它:

declare type JQuery = any;
declare var $: JQuery;
这提供给你一个更清晰的使用模式。

再一次说明,一个高质量的 jquery.d.ts 已经在 DefinitelyTyped 中存在。现在你已经知道当你使用 JavaScript 第三方模块时, 如何克服从 JavaScript TypeScript 的阻力。在接下去的内容,我们将会讨论环境声明。

@types

你可以通过 npm 来安装使用 @types,如下例所示,你可以为 jquery 添加声明文件:

npm install @types/jquery --save-dev

@types 支持全局和模块类型定义

安装完之后,不需要特别的配置,你就可以像使用模块一样使用它:

import * as $ from 'jquery';

变量

举个例子,当你想告诉 TypeScript 编辑器关于 process 变量时,你可以这么做:

declare let process: any

TIP

你并不需要为 process 做这些,因为这已经存在于社区维护的 node.d.ts

这允许你使用 process,并能成功通过 TypeScript

process.exit();

推荐尽可能的使用接口,例如:

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

declare let process: Process;

类实现接口:

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

class MyPoint implements Point {
  x: number;
  y: number; // Same as Point
}

枚举

枚举是组织收集有关联变量的一种方式,其他语言都有,所以TS中也加入了这个功能

enum CardSuit {
  Clubs,
  Diamonds,
  Hearts,
  Spades
}

// 简单的使用枚举类型
let Card = CardSuit.Clubs;

// 类型安全
Card = 'not a member of card suit'; // Error: string 不能赋值给 `CardSuit` 类型



enum Tristate {
  False,
  True,
  Unknown
}
编译成 JavaScript:
var Tristate;
(function(Tristate) {
  Tristate[(Tristate['False'] = 0)] = 'False';
  Tristate[(Tristate['True'] = 1)] = 'True';
  Tristate[(Tristate['Unknown'] = 2)] = 'Unknown';
})(Tristate || (Tristate = {}));

这意味着我们可以跨文件、模块拆分枚举定义~

enum Color {
  Red,
  Green,
  Blue
}

enum Color {
  DarkRed = 3,
  DarkGreen,
  DarkBlue
}
TIP:你应该在枚举的延续块中,初始化第一个成员,以便生成的代码不是先前定义的枚举类型值。TypeScript 将会发出警告,如果你定义初始值

函数声明:

type LongHand = {
  (a: number): number;
};

type ShortHand = (a: number) => number;


可调用的


interface ReturnString {
  (): string;
}

箭头函数


const simple: (foo: number) => string = foo => foo.toString();

TIP

它仅仅只能做为简单的箭头函数,你无法使用重载。如果想使用它,你必须使用完整的 { (someArgs): someReturn } 的语法

可实例化:

interface CallMeWithNewToGetString {
  new (): string;
}
// 使用
declare const Foo: CallMeWithNewToGetString;
const bar = new Foo(); // bar 被推断为 string 类型

类型断言:

推荐只使用统一的as foo 语法,而不是<foo>

初始用法:


let foo: any;
let bar = <string>foo; // 现在 bar 的类型是 'string'

然而,当你在 JSX 中使用 <foo> 的断言语法时,这会与 JSX 的语法存在歧义:

let foo = <string>bar;</string>;

因此,为了一致性,我们建议你使用 as foo 的语法来为类型断言

类型断言和类型转换

它之所以不被称为「类型转换」,是因为转换通常意味着某种运行时的支持。但是,类型断言纯粹是一个编译时语法,同时,它也是一种为编译器提供关于如何分析代码的方法

类型断言通常被认为是有害的

在很多情景下,断言能让你更容易的从遗留项目中迁移(甚至将其他代码粘贴复制到你的项目中),然而,你应该小心谨慎的使用断言。让我们用最初的代码做为示例,如果你没有按约定添加属性,TypeScript 编译器并不会对此发出错误警告:

interface Foo {
  bar: number;
  bas: string;
}

const foo = {} as Foo;

// ahhh, 忘记了什么?
上面的foo,并没有bar和bas属性,但是通过了检验。这是相当危险的,那熟悉的xx from undefined 报错

双重断言

类型断言,尽管我们已经证明了它并不是那么安全,但它也还是有用武之地。如下一个非常实用的例子所示,当使用者了解传入参数更具体的类型时,类型断言能按预期工作:

function handler(event: Event) {
  const mouseEvent = event as MouseEvent;
}

然而,如下例子中的代码将会报错,尽管使用者已经使用了类型断言:

function handler(event: Event) {
  const element = event as HTMLElement; // Error: 'Event' 和 'HTMLElement' 中的任何一个都不能赋值给另外一个
}

如果你仍然想使用那个类型,你可以使用双重断言。首先断言成兼容所有类型的 any,编译器将不会报错:

function handler(event: Event) {
  const element = (event as any) as HTMLElement; // ok
}

TypeScript 是怎么确定单个断言是否足够

S 类型是 T 类型的子集,或者 T 类型是 S 类型的子集时,S 能被成功断言成 T。这是为了在进行类型断言时提供额外的安全性,完全毫无根据的断言是危险的,如果你想这么做,你可以使用 any

Freshness

为了能让检查对象字面量类型更容易,TypeScript 提供 「Freshness」 的概念(它也被称为更严格的对象字面量检查)用来确保对象字面量在结构上类型兼容。

结构类型非常方便。考虑如下例子代码,它可以让你非常便利的从 JavaScript 迁移至 TypeScript,并且会提供类型安全:


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

const person = { name: 'matt', job: 'being awesome' };
const animal = { name: 'cow', diet: 'vegan, but has milk of own specie' };
const randow = { note: `I don't have a name property` };

logName(person); // ok
logName(animal); // ok
logName(randow); // Error: 没有 `name` 属性

但是,结构类型有一个缺点,它能误导你认为某些东西接收的数据比它实际的多。如下例,TypeScript 发出错误警告:

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

logName({ name: 'matt' }); // ok
logName({ name: 'matt', job: 'being awesome' }); // Error: 对象字面量只能指定已知属性,`job` 属性在这里并不存在。
WARNING
请注意,这种错误提示,只会发生在对象字面量上

允许分配而外的属性:

一个类型能够包含索引签名,以明确表明可以使用额外的属性:

let x: { foo: number, [x: string]: any };
x = { foo: 1, baz: 2 }; // ok, 'baz' 属性匹配于索引签名

readonly在React中


interface Props {
  readonly foo: number;
}

interface State {
  readonly bar: number;
}

export class Something extends React.Component<Props, State> {
  someMethod() {
    // 你可以放心,没有人会像下面这么做
    this.props.foo = 123; // Error: props 是不可变的
    this.state.baz = 456; // Error: 你应该使用 this.setState()
  }
}

泛型

// 创建一个泛型类
class Queue<T> {
  private data: T[] = [];
  push = (item: T) => this.data.push(item);
  pop = (): T | undefined => this.data.shift();
}

// 简单的使用
const queue = new Queue<number>();
queue.push(0);
queue.push('1'); // Error:不能推入一个 `string`,只有 number 类型被允许
你可以随意调用泛型参数,当你使用简单的泛型时,泛型常用 T、U、V 表示。如果在你的参数里,不止拥有一个泛型,你应该使用一个更语义化名称,如 TKey TValue (通常情况下,以 T 作为泛型的前缀,在其他语言如 C++ 里,也被称为模板)

变体

对类型兼容性来说,变体是一个利于理解和重要的概念。

对一个简单类型 BaseChild 来说,如果 Child Base 的子类,Child 的实例能被赋值给 Base 类型的变量。

Never

never 类型是 TypeScript 中的底层类型。它自然被分配的一些例子:

一个从来不会有返回值的函数(如:如果函数内含有 while(true) {})

一个总是会抛出错误的函数(如:function foo() { throw new Error('Not Implemented') },foo 的返回类型是 never

你也可以将它用做类型注解:

let foo: never; // ok
但是,never 类型仅能被赋值给另外一个 never:
let foo: never = 123; // Error: number 类型不能赋值给 never 类型

// ok, 做为函数返回类型的 never
let bar: never = (() => {
  throw new Error('Throw my hands in the air like I just dont care');
})();

与 void 的差异

一旦有人告诉你,never 表示一个从来不会优雅的返回的函数时,你可能马上就会想到与此类似的 void,然而实际上,void 表示没有任何类型,never 表示永远不存在的值的类型。

当一个函数没有返回值时,它返回了一个 void 类型,但是,当一个函数根本就没有返回值时(或者总是抛出错误),它返回了一个 never,void 指可以被赋值的类型(在 strictNullCheckingfalse 时),但是 never 不能赋值给其他任何类型,除了 never

TypeScript 索引签名

JavaScript 在一个对象类型的索引签名上会隐式调用 toString 方法,而在 TypeScript 中,为防止初学者砸伤自己的脚(我总是看到 stackoverflow 上有很多 JavaScript 使用者都会这样。),它将会抛出一个错误。

const obj = {
  toString() {
    return 'Hello';
  }
};

const foo: any = {};

// ERROR: 索引签名必须为 string, number....
foo[obj] = 'World';

// FIX: TypeScript 强制你必须明确这么做:
foo[obj.toString()] = 'World';

声明一个索引签名

我们通过使用 any 来让 TypeScript 允许我们可以做任意我们想做的事情。实际上,我们可以明确的指定索引签名。例如:假设你想确认存储在对象中任何内容都符合 { message: string } 的结构,你可以通过 [index: string]: { message: string }来实现。

const foo: {
  [index: string]: { message: string };
} = {};

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

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

// 读取时,也会有类型检查
// ok
foo['a'].message;

// Error: messages 不存在
foo['a'].messages;

TIP

索引签名的名称(如:{ [index: string]: { message: string } } 里的 index )除了可读性外,并没有任何意义。例如:如果有一个用户名,你可以使用 { username: string}: { message: string },这有利于下一个开发者理解你的代码。
当你声明一个索引签名时,所有明确的成员都必须符合索引签名:
// ok
interface Foo {
  [key: string]: number;
  x: number;
  y: number;
}

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

至此,4000字简单介绍了TypeScript的基础内容部分,当然,这里每个部分都可以被拓展出来讲很久。需要大家认真去看《深入理解TypeScript

下一章,针对TypeScript的原理、工程化环境等进行进阶编写~

写在最后:

觉得写得不错,欢迎关注微信公众号:前端巅峰

回复:进群 即可加入小姐姐多多多多的大前端交流群~


PeterTan
14.4k 声望30k 粉丝