《深入理解TypeScript》读后感一篇【基础篇】_赋值

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

《深入理解TypeScript》读后感一篇【基础篇】_jquery_02

在读完《深入理解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++ 里,也被称为模板)

变体

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

对一个简单类型​​ Base​​​ 和 ​​Child ​​​来说,如果 ​​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 指可以被赋值的类型(在 ​​strictNullChecking​​​ 为 ​​false​​​时),但是​​ 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​​的原理、工程化环境等进行进阶编写~