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++ 里,也被称为模板)
变体
对类型兼容性来说,变体是一个利于理解和重要的概念。
对一个简单类型 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
的原理、工程化环境等进行进阶编写~
写在最后:
觉得写得不错,欢迎关注微信公众号:前端巅峰
回复:进群
即可加入小姐姐多多多多的大前端交流群~
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。