什么是 TypeScript
官网的定义
TypeScript is a typed superset of JavaScript that compiles to plain JavaScript. Any browser. Any host. Any OS. Open source.
TypeScript 是 JavaScript 的类型的超集,它可以编译成纯 JavaScript。编译出来的 JavaScript 可以运行在任何浏览器上。TypeScript 编译工具可以运行在任何服务器和任何系统上。TypeScript 是开源的。
个人理解
TypeScript 是 JavaScript 的一个超集,主要提供了类型系统。我感觉不要把 TypeScript 认为是一门新的语言,把它看做是 JavaScript 的一个超集,一个提升 JavaScript 代码质量的工具。"一切能用 JavaScript 实现的东西,都将会用 JavaScript 实现。"
为什么选择 TypeScript
简单来说,因为 JavaScript 设计之初就有一些缺陷,缺少一些构建大型应用必备的基础特性。比如:
- 到现在也没解决的类型问题
- 结构化机制(类、模块、接口等等)
- ...
这就导致了一些问题,比如代码可读性比较差,后期维护成本高,重构也比较麻烦。因为缺乏类型检查,更容易出现低级错误。而 TypeScript 从名字上就能看出,他最大的特点之一就是类型化,可以在代码编译期间提供静态类型检查
,可以更早的发现代码中出现的错误,可以省去很多 debug 的时间。
function sayHi(user) {
console.log(use.name);
}
sayHi();
// Uncaught TypeError
有没有很熟悉,虽然说这个是很低级的错误,一般情况下是不会出现的(嗯,假装我是老司机)。但是一旦不小心出现了,忘记传值,系统并不会告诉你,一定要等到执行阶段才会发现。所以当项目代码多起来,复杂起来,就难免会出现这样的情况。就像这个 demo 里的一样,我在写的时候压根不会出现任何异常,基本要到运行的时候才能发现,最糟糕的是到如果线上才发现,那就麻烦了。
TypeScript | JavaScript |
---|---|
JavaScript的超集用于解决大型项目的代码复杂性 | 一种脚本语言,用于创建动态网页 |
可以在编译期间发现并纠正错误 | 作为一种解释型语言,只能在运行时发现错误 |
强类型,支持静态和动态类型 | 弱类型,没有静态类型选项 |
最终被编译成JavaScript代码,使浏览器可以理解 | 可以直接在浏览器中使用 |
支持模块、泛型和接口 | 不支持模块,泛型或接口 |
支持 ES3,ES4,ES5 和 ES6 等 | 不支持编译其他 ES3,ES4,ES5 或 ES6 功能 |
社区的支持仍在增长,而且还不是很大 | 大量的社区支持以及大量文档和解决问题的支持 |
TypeScript 的缺点
任何事物都是有两面性的,我认为 TypeScript 的弊端在于:
- 有一定的学习成本,需要理解接口(Interfaces)、泛型(Generics)、类(Classes)、枚举类型(Enums)等前端工程师可能不是很熟悉的概念
- 短期可能会增加一些开发成本,毕竟要多写一些类型的定义,不过对于一个需要长期维护的项目,TypeScript 能够减少其维护成本
- 集成到构建流程需要一些工作量
- 可能和一些库结合的不是很完美
安装
获取 TypeScript
TypeScript 的命令行工具安装方法如下:
npm install -g typescript
编译 TypeScript 文件
以上命令会在全局环境下安装 tsc
命令,安装完成之后,我们就可以在任何地方执行 tsc
命令了。
编译一个 TypeScript 文件很简单:
tsc index.ts
我们约定使用 TypeScript 编写的文件以 .ts
为后缀,用 TypeScript 编写 React 时,以 .tsx
为后缀。
如何制动编译 TypeScript 文件
tsc --init
// 将tsconfig.json文件暴露出来,如下图
Vscode编辑器->终端->运行任务->typeScript->tsc:监视 然后选中
选中后你的终端就会实时编辑ts成js
基础
变量声明
TypeScript 是 JavaScript 的超集,在变量声明方式方面,关键字是和 JavaScript 保持一致的,并且推荐较新的 let
和 const
代替 var
。区别就在于,TypeScript 在定义变量时会声明变量的类型。
布尔
布尔值是最基础的数据类型,在 TypeScript 中,使用 boolean
定义布尔值类型
let isDone: boolean = false;
// var isDone = false;
也许会有人问: boolean
,是不是必须得写呢?这里提前透露一下,有些时候也不是必须的。
数值
使用 number
定义数值类型
let decLiteral: number = 6;
let hexLiteral: number = 0xf00d;
// ES6 中的二进制表示法
let binaryLiteral: number = 0b1010;
// ES6 中的八进制表示法
let octalLiteral: number = 0o744;
let notANumber: number = NaN;
let infinityNumber: number = Infinity;
// var decLiteral = 6;
// var hexLiteral = 0xf00d;
// ES6 中的二进制表示法
// var binaryLiteral = 10;
// ES6 中的八进制表示法
// var octalLiteral = 484;
// var notANumber = NaN;
// var infinityNumber = Infinity;
字符串
使用 string
定义字符串类型
let myName: string = "Tom";
let myAge: number = 25;
// 模板字符串
let sentence: string = `Hello, my name is ${myName}.
I'll be ${myAge + 1} years old next month.`;
// var myName = 'Tom';
// var myAge = 25;
// 模板字符串
// var sentence = "Hello, my name is " + myName + ".
// I'll be " + (myAge + 1) + " years old next month.";
Null 和 Undefined
使用 null
和 undefined
来定义这两个原始数据类型
let u: undefined = undefined;
let n: null = null;
//var u = undefined;
//var n = null;
空值
JavaScript 没有空值(Void)的概念,在 TypeScript 中,可以用 void
表示没有任何返回值的函数
function alertName(): void {
alert("My name is Ben");
}
// 声明一个 void 类型的变量没有什么用,因为你只能将它赋值为 undefined 和 null
let unusable: void = undefined;
与 null
和 undefined
的区别是,默认情况下 null
和 undefined
是所有类型的子类型。也就是说你可以把 null
和 undefined
赋值给 number
类型的变量。
// 这样不会报错
let num: number = undefined;
// 这样也不会报错
let u: undefined;
let num: number = u;
//error!
let u: void;
let num: number = u;
任意值
万能的 any 大法,哈哈。在 TypeScript 中,任何类型都可以被归为 any
类型。这让 any
类型成为了类型系统的顶级类型(也被称作全局超级类型)
let notSure: any = 666;
notSure = "Semlinker";
notSure = false;
any
类型本质上是类型系统的一个逃逸舱。作为开发者,这给了我们很大的自由:TypeScript 允许我们对 any
类型的值执行任何操作,而无需事先执行任何形式的检查。可以认为,声明一个变量为任意值之后,对它的任何操作,返回的内容的类型都是任意值。
let value: any;
value.foo.bar; // OK
value.trim(); // OK
value(); // OK
new value(); // OK
value[0][1]; // OK
在许多场景下,这太宽松了。使用 any
类型,可以很容易地编写类型正确但在运行时有问题的代码。如果我们使用 any
类型,就无法使用 TypeScript 提供的大量的保护机制。为了解决 any
带来的问题,TypeScript 引入了 unknown
类型。
Unknown 类型
就像所有类型都可以赋值给 any
,所有类型也都可以赋值给 unknown
。这使得 unknown
成为 TypeScript 类型系统的另一种顶级类型。
let value: unknown;
value = true; // OK
value = 42; // OK
value = "Hello World"; // OK
value = []; // OK
value = {}; // OK
value = Math.random; // OK
value = null; // OK
value = undefined; // OK
value = new TypeError(); // OK
value = Symbol("type"); // OK
对 value
变量的所有赋值都被认为是类型正确的。但是,当我们尝试将类型为 unknown
的值赋值给其他类型的变量时会发生什么?
let value: unknown;
let value1: unknown = value; // OK
let value2: any = value; // OK
let value3: boolean = value;
let value4: number = value;
let value5: string = value;
let value6: object = value;
let value7: any[] = value;
let value8: Function = value;
unknown
类型只能被赋值给any
类型和unknown
类型本身。直观地说,这是有道理的:只有能够保存任意类型值的容器才能保存unknown
类型的值。毕竟我们不知道变量value
中存储了什么类型的值。- 现在让我们看看当我们尝试对类型为
unknown
的值执行操作时会发生什么。以下是我们在之前 any 类型看过的相同操作
let value: unknown;
value.foo.bar;
value.trim();
value();
new value();
value[0][1];
将 value
变量类型设置为 unknown
后,这些操作都不再被认为是类型正确的。通过将 any
类型改变为 unknown
类型,我们已将允许所有更改的默认设置,更改为禁止任何更改。
数组类型
在 TypeScript 中,数组类型有多种定义方式,比较灵活。
最简单的方法是使用「类型 + 方括号」来表示数组
let list: number[] = [1, 2, 3];
// ES5:var list = [1,2,3];
list.push("8");
// Argument of type '"8"' is not assignable to parameter of type 'number'.
数组泛型
let fibonacci: Array<number> = [1, 1, 2, 3, 5];
用接口表示数组
interface NumberArray {
[index: number]: number;
}
let fibonacci: NumberArray = [1, 1, 2, 3, 5];
//NumberArray 表示:只要索引的类型是数字时,那么值的类型必须是数字。
> ```
## 进阶
### 类型推论
如果没有明确的指定类型,那么 TypeScript 会依照类型推论(Type Inference)的规则推断出一个类型。
```javascript
let myFavoriteNumber = "seven";
myFavoriteNumber = 7;
// index.ts(2,1): error TS2322: Type 'number' is not assignable to type 'string'.
事实上,它等价于:
let myFavoriteNumber: string = "seven";
myFavoriteNumber = 7;
// index.ts(2,1): error TS2322: Type 'number' is not assignable to type 'string'.
如果定义的时候没有赋值,不管之后有没有赋值,都会被推断成 any
类型而完全不被类型检查。
let myFavoriteNumber;
myFavoriteNumber = "seven"; //OK
myFavoriteNumber = 7; //OK
联合类型
联合类型(Union Types)表示取值可以为多种类型中的一种。联合类型使用 |
分隔每个类型。
let toggle: "on" | "off" = "on";
//toggle只能取"on" 或 "off"之一
toggle = "off";
toggle = "other";
let myFavoriteNumber: string | number;
//允许 myFavoriteNumber 的类型是 string 或者 number,但是不能是其他类型
myFavoriteNumber = "seven";
myFavoriteNumber = 7;
myFavoriteNumber = true;
访问联合类型的属性或方法
当 TypeScript 不确定一个联合类型的变量到底是哪个类型的时候,我们只能访问此联合类型的所有类型里共有的属性或方法。
function getLength(something: string | number): number {
return something.length;
}
// index.ts(2,22): error TS2339: Property 'length' does not exist on type 'string | number'.
// Property 'length' does not exist on type 'number'.
访问 string
和 number
的共有属性是没问题的
function getString(something: string | number): string {
return something.toString();
}
联合类型的变量在被赋值的时候,会根据类型推论的规则推断出一个类型
let myFavoriteNumber: string | number;
myFavoriteNumber = 'seven';
console.log(myFavoriteNumber.length); // 5
myFavoriteNumber = 7;
console.log(myFavoriteNumber.length); // 编译时报错
// index.ts(5,30): error TS2339: Property 'length' does not exist on type 'number'.
}
类型断言
之前提到过,当 TypeScript 不确定一个联合类型的变量到底是哪个类型的时候,我们只能访问此联合类型的所有类型中共有的属性或方法:
interface Cat {
name: string;
run(): void;
}
interface Fish {
name: string;
swim(): void;
}
function getName(animal: Cat | Fish) {
return animal.name;
}
而有时候,我们确实需要在还不确定类型的时候就访问其中一个类型特有的属性或方法,语法值 as 类型或 <类型>值
interface Cat {
name: string;
run(): void;
}
interface Fish {
name: string;
swim(): void;
}
function isFish(animal: Cat | Fish) {
if (typeof animal.swim === "function") {
return true;
}
return false;
}
as
和<>
两种形式是等价的。 在 TypeScript 里使用JSX
时,只有as
语法断言是被允许的。
类型别名
我们使用 type
创建类型别名,类型别名常用于联合类型。
type Name = string;
type NameResolver = () => string;
type NameOrResolver = Name | NameResolver;
function getName(n: NameOrResolver): Name {
if (typeof n === "string") {
return n;
} else {
return n();
}
}
接口
简单理解,接口就是用来描述对象或者类的具体结构,约束他们的行为。这也是 TypeScript 的核心原则之一:对结构进行类型检查。在实际代码设计中也很有好处,增强了系统的可拓展性和可维护性。
接口定义
和其他语言类似, TypeScript 中接口也是使用 interface
关键字来定义的。
interface Person {
name: string;
age: number;
}
// person必须有name和age属性
const person: Person = {
name: "Max",
age: 21,
};
可选和只读属性
定义时在属性值后面加个?
就代表这个属性是可选的。TypeScript 还支持定义只读属性,在属性前用 readonly
指定就行。
interface Person {
name: string;
age?: number;
readonly sex: string;
}
// age 属性可选
const stu: Person = { name: "Amy", sex: "male" };
stu.sex="female"
任意属性
有时候我们希望一个接口允许有任意属性,可以使用[propName: string]
。
interface Person {
name: string;
age?: number;
[propName: string]: any;
}
const person: Person = {
name: "Max",
sex: "male",
};
接口继承
接口继承使用extends
关键字,可以让我们更方便灵活的复用。
interface Shape {
color: string;
}
interface Square extends Shape {
sideLength: number;
}
let square = {} as Square;
square.color = "blue";
square.sideLength = 10;
一个接口可以继承多个接口, 使用 ,
分隔
interface Shape {
color: string;
}
interface PenStroke {
penWidth: number;
}
interface Square extends Shape, PenStroke {
sideLength: number;
}
let square = {} as Square;
square.color = "blue";
square.sideLength = 10;
square.penWidth = 5.0;
interface vs type
共同点:
-
两者都可以用来描述对象或函数的类型
不同点: -
类型别名还可以用于其他类型,如基本类型(原始值)、联合类型、元组
-
类型别名不能被 extends
-
type 能使用 in 关键字生成映射类型。interface 不可以
type Keys = "ym" | "yn" type NameType = { [key in Keys]: string } const demo: NameType = { ym: "name1", yn: "name2" }
函数
TypeScript | JavaScript |
---|---|
含有类型 | 无类型 |
必填和可选参数 | 所有参数都是可选的 |
函数重载 | 无函数重载 |
函数定义
和 JavaScript 一样,TypeScript 函数可以创建有名字的函数和匿名函数。我们可以给每个参数添加类型之后再为函数本身添加返回值类型。 TypeScript 能够根据返回语句自动推断出返回值类型,因此我们通常省略它。
// Named function
function add(x: number, y: number): number {
return x + y;
}
// Anonymous function
const myAdd = function (x: number, y: number): number {
return x + y;
};
//输入多余的(或者少于要求的)参数,是不被允许的
add(1);
add(1, 2, 3);
用接口定义函数的形状
interface SearchFunc {
(source: string, subString: string): boolean;
}
let mySearch: SearchFunc = function (source: string, subString: string) {
return source.search(subString) !== -1;
};
可选参数和默认参数
JavaScript 里每个参数都是可选的,可传可不传。 没传参的时候,它的值就是undefined
。 在 TypeScript 里我们可以在参数名旁使用 ?
实现可选参数的功能。可选参数必须跟在必须参数后面。
function buildName(firstName: string, lastName?: string) {
if (lastName) return firstName + " " + lastName;
else return firstName;
}
let result1 = buildName("Bob"); // works correctly now
let result2 = buildName("Bob", "Adams", "Sr."); // error, too many parameters
let result3 = buildName("Bob", "Adams"); // ah, just right
在 TypeScript 里,我们也可以为参数提供一个默认值当用户没有传递这个参数或传递的值是 undefined
时。也就是说 TypeScript 会将添加了默认值的参数识别为可选参数(参考 ES6 中函数参数的默认值)。
function buildName(firstName: string, lastName = "Smith") {
return firstName + " " + lastName;
}
let result1 = buildName("Bob"); // works correctly now, returns "Bob Smith"
let result2 = buildName("Bob", undefined); // still works, also returns "Bob Smith"
let result3 = buildName("Bob", "Adams", "Sr."); // error, too many parameters
let result4 = buildName("Bob", "Adams");
与普通可选参数不同的是,带默认值的参数不需要放在必须参数的后面。 如果带默认值的参数出现在必须参数前面,用户必须明确的传入
undefined
值来获得默认值。
函数重载???
let suits = ["hearts", "spades", "clubs", "diamonds"];
function pickCard(x: {suit: string; card: number; }[]): number;
function pickCard(x: number): {suit: string; card: number; };
function pickCard(x): any {
// Check to see if we're working with an object/array
// if so, they gave us the deck and we'll pick the card
if (typeof x == "object") {
let pickedCard = Math.floor(Math.random() * x.length);
return pickedCard;
}
// Otherwise just let them pick the card
else if (typeof x == "number") {
let pickedSuit = Math.floor(x / 13);
return { suit: suits[pickedSuit], card: x % 13 };
}
}
let myDeck = [{ suit: "diamonds", card: 2 }, { suit: "spades", card: 10 }, { suit: "hearts", card: 4 }];
let pickedCard1 = myDeck[pickCard(myDeck)];
alert("card: " + pickedCard1.card + " of " + pickedCard1.suit);
let pickedCard2 = pickCard(15);
alert("card: " + pickedCard2.card + " of " + pickedCard2.suit);
枚举
使用枚举我们可以定义一些带名字的常量。 使用枚举可以清晰地表达意图或创建一组有区别的用例。 TypeScript 支持数字的和基于字符串的枚举。枚举使用 enum
关键字来定义。
enum Direction {
Up,
Down,
Left,
Right,
}
反向映射
数字型的枚举可以映射,字符串类型的枚举是不可以映射的
enum Enum {A,B}
enum Strs {A = 'apple',B = 'orange'}
console.log(Enum);
console.log(Strs);
枚举类型
当所有枚举成员都拥有字面量枚举值,枚举成员成为了类型,并且枚举类型本身变成了每个枚举成员的 联合
- 字面量枚举成员:
- 任何字符串字面量(例如: "foo", "bar", "baz")
- 任何数字字面量(例如: 1, 100)
- 应用了一元
-
符号的数字字面量(例如: -1, -100)
- 数字类型枚举
enum ShapeKind {
Circle,
Square,
}
interface Circle {
kind: ShapeKind.Circle;
radius: number;
}
let c: Circle = {
kind: ShapeKind.Square,
// 此处可以是`kind: ShapeKind.Circle` 或者是任意数字类型例如`kind: 10`
radius: 100,
};
- 联合枚举类型
枚举类型本身变成了每个枚举成员的 联合
enum E {
Foo,
Bar,
}
function f(x: E) {
if (x !== E.Foo || x !== E.Bar) {}
//此条件将始终返回 "true",因为类型 "E.Foo" 和 "E.Bar" 没有重叠
}
这个例子里,我们先检查x
是否不是E.Foo
。如果通过了这个检查,然后||
会发生短路效果,if
语句体里的内容会被执行。然而,这个检查没有通过,那么x
则只能为E.Foo
,因此没理由再去检查它是否为E.Bar
。
泛型
在像 C#和 Java 这样的语言中,可以使用泛型来创建可重用的组件,一个组件可以支持多种类型的数据。 这样用户就可以以自己的数据类型来使用组件。
function identity(arg: any): any {
return arg;
}
使用 any
类型会导致这个函数可以接收任何类型的 arg
参数,这样就丢失了一些信息:传入的类型与返回的类型应该是相同的。
如果我们传入一个数字,我们只知道任何类型的值都有可能被返回。
function identity<T>(arg: T): T {
return arg;
}
我们给 identity
添加了类型变量 T
。 T
帮助我们捕获用户传入的类型(比如:number
),之后我们再次使用了 T
当做返回值类型。
现在我们可以知道参数类型与返回值类型是相同的了。
我们把这个版本的identity
函数叫做泛型,因为它可以适用于多个类型。 不同于使用 any
,它不会丢失信息。
React 和 TypeScript 如何一起使用
项目初始化
初始化一个 React/TypeScript
应用程序的最快方法是 create-react-app
与 TypeScript 模板一起使用。你可以运行以下面的命令:
npx create-react-app my-app --template typescript
这可以让你开始使用 TypeScript 编写 React 。一些明显的区别是:
.tsx
:TypeScript JSX 文件扩展
tsconfig.json
:具有一些默认配置的 TypeScript 配置文件
react-app-env.d.ts
:TypeScript 声明文件,可以进行允许引用 SVG 这样的配置
tsconfig.json
{
"compilerOptions": {
"target": "es5", // 指定 ECMAScript 版本
"lib": [
"dom",
"dom.iterable",
"esnext"
], // 要包含在编译中的依赖库文件列表
"allowJs": true, // 允许编译 JavaScript 文件
"skipLibCheck": true, // 跳过所有声明文件的类型检查
"esModuleInterop": true, // 禁用命名空间引用 (import * as fs from "fs") 启用 CJS/AMD/UMD 风格引用 (import fs from "fs")
"allowSyntheticDefaultImports": true, // 允许从没有默认导出的模块进行默认导入
"strict": true, // 启用所有严格类型检查选项
"forceConsistentCasingInFileNames": true, // 不允许对同一个文件使用不一致格式的引用
"module": "esnext", // 指定模块代码生成
"moduleResolution": "node", // 使用 Node.js 风格解析模块
"resolveJsonModule": true, // 允许使用 .json 扩展名导入的模块
"noEmit": true, // 不输出(意思是不编译代码,只执行类型检查)
"jsx": "react", // 在.tsx文件中支持JSX
"sourceMap": true, // 生成相应的.map文件
"declaration": true, // 生成相应的.d.ts文件
"noUnusedLocals": true, // 报告未使用的本地变量的错误
"noUnusedParameters": true, // 报告未使用参数的错误
"experimentalDecorators": true, // 启用对ES装饰器的实验性支持
"incremental": true, // 通过从以前的编译中读取/写入信息到磁盘上的文件来启用增量编译
"noFallthroughCasesInSwitch": true
},
"include": [
"src/**/*" // *** TypeScript文件应该进行类型检查 ***
],
"exclude": ["node_modules", "build"] // *** 不进行类型检查的文件 ***
}
组件
import React from "react";
// 函数声明式写法
function Heading(): React.ReactNode {
return <h1>My Website Heading</h1>;
}
// 函数扩展式写法
const OtherHeading: React.FC = () => <h1>My Website Heading</h1>;
注意这里的关键区别。在第一个例子中,使用函数声明式写法,注明了这个函数返回值是 React.ReactNode
类型。相反,第二个例子使用了一个函数表达式。因为第二个实例返回一个函数,而不是一个值或表达式,所以注明了这个函数返回值是 React.FC
类型。
开发时,两种写法都可以,但开发团队需要保持一致,只采取一种。
Props
你可以使用 interface
或 type
来定义 Props。
import React from "react";
interface Props {
name: string;
color: string;
}
type OtherProps = {
name: string,
color: string,
};
// Notice here we're using the function declaration with the interface Props
function Heading({ name, color }: Props): React.ReactNode {
return <h1>My Website Heading</h1>;
}
// Notice here we're using the function expression with the type OtherProps
const OtherHeading: React.FC<OtherProps> = ({ name, color }) => (
<h1>My Website Heading</h1>
);