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 中使用 的断言语法时,这会与 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的原理、工程化环境等进行进阶编写~
写在最后:
###觉得写得不错,欢迎关注微信公众号:前端巅峰
回复:进群 即可加入小姐姐多多多多的大前端交流群~