TS简介
Typescript和JavaScript的区别 Typescript是JavaScript的 类型化超集,它支持所有JavaScript的语法,并在此基础上添加静态类型定义和面向对象的思想。最终编译成JavaScript运行。
Javascript 动态类型,运行时明确变量的类型,变量的类型由变量的值决定,并跟随值的改变而改变; 直接运行在浏览器和node.js环境中; 弱类型,数据类型可以被忽略的语言。一个变量可以赋不同数据类型的值; Typescript 静态类型,声明时确定类型,之后不允许修改; 编译运行,始终先编译成JavaScript再运行; 强类型,一旦一个变量被指定了某个数据类型,如果不经过强制转换,那么它就永远是这个数据类型了; 使用typescript的好处 开源,跨平台。它本身不需要考虑运行环境的问题,所有支持JavaScript的地方都可以使用typescript; 引入静态类型声明,减少不必要的类型判断和文档注释; 及早发现错误,静态类型检查或编译时发现问题,不用等到运行; 类、接口的使用更易于构建和维护组件; 重构更方便可靠,适合大型项目;
从宏观的视角来看,TypeScript 的功能就是:
- 提供了丰富的类型系统。
最简单的就是 变量名:类型 = 值
const a: Number = 1;
除了这些基本类型,还提供了函数类型,复合类型等。
- 提供了类型操作 API。TypeScript 不但提供内置类型,用户也可以利用集合操作和泛型对类型操作从而生成新的类型。
- 对每一种类型的属性和方法都进行了定义。
比如 String 类型有 toString 方法,但是没有 toFixed 方法,这就是 lib.d.ts 定义的。这样我在 String 类型的变量上使用 toFixed 方法就会报错,达到了“类型检查”的作用。
小提示:lib.d.ts 的内容主要是一些变量声明(如:window、document、math)和一些类似的接口声明(如:Window、Document、Math)。 你可以通过 --noLib 来关闭这一功能
- 提供了模块系统(module,namespace)。
- 提供了更加方面的 API,比如 class(这在 ES6 class 出来之前尤其好用),装饰器等。
TypeScript 编译器是如何工作的?
- TypeScript 文本首先会被解析为 token 流。这个过程比较简单,就是单纯地按照分隔符去分割文本即可。
- 接着 token 流会被转换为 AST,也就是抽象语法树。
- binder 则根据 AST 信息生成 Symbol。拿上面的图来说,就是 number 节点。
- 当我们需要类型检查的时候, checker 会根据前面生成的 AST 和 symbols 生成类型检查结果。
- 当我们需要生成 JS 文件的时候,emitter 同样会根据前面生成的 AST 和 symbols 生成 JS 文件。
完整图:
1、变量类型和值类型
-
JavaScript 中的类型其实是值的类型。实际上不仅仅是 JavaScript,任何动态类型语言都是如此,这也是动态类型语言的本质。
对于 JavaScript 来说,一个变量可以是任意类型。
var a = 1; a = "lucifer"; a = {}; a = [];
-
Typescript 中的类型其实是变量的类型。实际上不仅仅是 Typescript,任何静态类型语言都是如此,这也是静态类型语言的本质。
对于 Typescript 来说, 一个变量只能接受和它类型兼容的类型的值。
var a: number = 1;
a = "lucifer"; // error
var b: any = 1;
b = "lucifer"; // ok
b = {}; // ok
b = []; // ok
我们不能将 string 类型的值赋值给变量 a, 因为 string 和 number 类型不兼容。而我们可以将 string,Object,Array 类型的值赋值给 b,因此 它们和 any 类型兼容。简单来说就是,一旦一个变量被标注了某种类型,那么其就只能接受这个类型以及它的子类型。
2、类型空间和值空间
类型和值居住在不同的空间,一个在阳间一个在阴间。他们之间互相不能访问,甚至不知道彼此的存在。类型不能当做值来用,反之亦然。
类型空间
如下代码会报类型找不到的错:
const aa: User = { name: "lucifer", age: 17 };
这个比较好理解,我们只需要使用 interface 声明一下 User 就行。
interface User {
name: string;
age: number;
}
const aa: User = { name: "lucifer", age: 17 };
也就是说使用 interface 可以在类型空间声明一个类型,这个是 Typescript 的类型检查的基础之一。
实际上类型空间内部也会有子空间。我们可以用 namespace(老)和 module(新) 来创建新的子空间。子空间之间不能直接接触,需要依赖导入导出来交互。
值空间
比如,用 Typescript 写出如下的代码:
const a = window.lucifer();
Typescript 会报告一个类似Property 'lucifer' does not exist on type 'Window & typeof globalThis'.
的错误。
实际上,这种错误并不是类型错误,而是找不到成员变量的错误。我们可以这样解决:
declare var lucifer: () => any;
也就是说使用 declare 可以在值空间声明一个变量。这个是 Typescript 的变量检查的基础。
3、类型系统
类型系统包括 1. 类型 和 2.对类型的使用和操作。
类型
数组
TypeScript 为数组提供了专用的类型语法,因此你可以很轻易的注解数组。它使用后缀 []
, 接着你可以根据需要补充任何有效的类型注解(如::boolean[]
)。它能让你安全的使用任何有关数组的操作,而且它也能防止一些类似于赋值错误类型给成员的行为。如下所示:
let boolArray: boolean[];
boolArray = [true, false];
console.log(boolArray[0]); // true
console.log(boolArray.length); // 2
boolArray[1] = true;
boolArray = [false, false];
boolArray[0] = 'false'; // Error
boolArray = 'false'; // Error
boolArray = [true, 'false']; // Error
接口
接口是 TypeScript 的一个核心知识,它能合并众多类型声明至一个类型声明:
interface Name {
first: string;
second: string;
}
let name: Name;
name = {
first: 'John',
second: 'Doe'
};
name = {
// Error: 'Second is missing'
first: 'John'
};
name = {
// Error: 'Second is the wrong type'
first: 'John',
second: 1337
};
在这里,我们把类型注解:first: string
+ second: string
合并到了一个新的类型注解 Name
里,这样能强制对每个成员进行类型检查。接口在 TypeScript 拥有强大的力量,稍后,我们将会用一个内容专门阐述如何更好的使用它。
内联类型注解
与创建一个接口不同,你可以使用内联注解语法注解任何内容::{ /*Structure*/ }
:
let name: {
first: string;
second: string;
};
name = {
first: 'John',
second: 'Doe'
};
name = {
// Error: 'Second is missing'
first: 'John'
};
name = {
// Error: 'Second is the wrong type'
first: 'John',
second: 1337
};
内联类型能为你快速的提供一个类型注解。它可以帮助你省去为类型起名的麻烦(你可能会使用一个很糟糕的名称)。然而,如果你发现需要多次使用相同的内联注解时,那么考虑把它重构为一个接口(或者是 type alias
,它会在接下来的部分提到)是一个不错的主意。
特殊类型
除了被提到的一些原始类型,在 TypeScript 中,还存在一些特殊的类型,它们是 any
、 null
、 undefined
以及 void
。
any
any
类型在 TypeScript 类型系统中占有特殊的地位。它提供给你一个类型系统的「后门」,TypeScript 将会把类型检查关闭。在类型系统里 any
能够兼容所有的类型(包括它自己)。因此,所有类型都能被赋值给它,它也能被赋值给其他任何类型。以下有一个证明例子:
let power: any;
// 赋值任意类型
power = '123';
power = 123;
// 它也兼容任何类型
let num: number;
power = num;
num = power;
当你把 JavaScript 迁移至 TypeScript 时,你将会经常性使用 any
。但你必须减少对它的依赖,因为你需要确保类型安全。当使用 any
时,你基本上是在告诉 TypeScript 编译器不要进行任何的类型检查。
null 和 undefined
在类型系统中,JavaScript 中的 null 和 undefined 字面量和其他被标注了 any
类型的变量一样,都能被赋值给任意类型的变量,如下例子所示:
// strictNullChecks: false
let num: number;
let str: string;
// 这些类型能被赋予
num = null;
str = undefined;
void
使用 :void
来表示一个函数没有一个返回值
function log(message: string): void {
console.log(message);
}
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 类型可以被赋值(在 strictNullChecking 为 false 时),但是除了 never 本身以外,其他任何类型不能赋值给 never。
Enum 类型
使用枚举我们可以定义一些带名字的常量。 使用枚举可以清晰地表达意图或创建一组有区别的用例。 TypeScript 支持数字的和基于字符串的枚举。
1.数字枚举
enum Direction {
NORTH,
SOUTH,
EAST,
WEST,
}
let dir: Direction = Direction.NORTH;
复制代码
默认情况下,NORTH 的初始值为 0,其余的成员会从 1 开始自动增长。换句话说,Direction.SOUTH 的值为 1,Direction.EAST 的值为 2,Direction.WEST 的值为 3。
以上的枚举示例经编译后,对应的 ES5 代码如下:
"use strict";
var Direction;
(function (Direction) {
Direction[(Direction["NORTH"] = 0)] = "NORTH";
Direction[(Direction["SOUTH"] = 1)] = "SOUTH";
Direction[(Direction["EAST"] = 2)] = "EAST";
Direction[(Direction["WEST"] = 3)] = "WEST";
})(Direction || (Direction = {}));
var dir = Direction.NORTH;
复制代码
当然我们也可以设置 NORTH 的初始值,比如:
enum Direction {
NORTH = 3,
SOUTH,
EAST,
WEST,
}
复制代码
2.字符串枚举
在 TypeScript 2.4 版本,允许我们使用字符串枚举。在一个字符串枚举里,每个成员都必须用字符串字面量,或另外一个字符串枚举成员进行初始化。
enum Direction {
NORTH = "NORTH",
SOUTH = "SOUTH",
EAST = "EAST",
WEST = "WEST",
}
复制代码
以上代码对应的 ES5 代码如下:
"use strict";
var Direction;
(function (Direction) {
Direction["NORTH"] = "NORTH";
Direction["SOUTH"] = "SOUTH";
Direction["EAST"] = "EAST";
Direction["WEST"] = "WEST";
})(Direction || (Direction = {}));
复制代码
通过观察数字枚举和字符串枚举的编译结果,我们可以知道数字枚举除了支持 从成员名称到成员值 的普通映射之外,它还支持 从成员值到成员名称 的反向映射:
enum Direction {
NORTH,
SOUTH,
EAST,
WEST,
}
let dirName = Direction[0]; // NORTH
let dirVal = Direction["NORTH"]; // 0
复制代码
另外,对于纯字符串枚举,我们不能省略任何初始化程序。而数字枚举如果没有显式设置值时,则会使用默认规则进行初始化。
3.常量枚举
除了数字枚举和字符串枚举之外,还有一种特殊的枚举 —— 常量枚举。它是使用 const
关键字修饰的枚举,常量枚举会使用内联语法,不会为枚举类型编译生成任何 JavaScript。为了更好地理解这句话,我们来看一个具体的例子:
const enum Direction {
NORTH,
SOUTH,
EAST,
WEST,
}
let dir: Direction = Direction.NORTH;
复制代码
以上代码对应的 ES5 代码如下:
"use strict";
var dir = 0 /* NORTH */;
复制代码
4.异构枚举
异构枚举的成员值是数字和字符串的混合:
enum Enum {
A,
B,
C = "C",
D = "D",
E = 8,
F,
}
复制代码
以上代码对于的 ES5 代码如下:
"use strict";
var Enum;
(function (Enum) {
Enum[Enum["A"] = 0] = "A";
Enum[Enum["B"] = 1] = "B";
Enum["C"] = "C";
Enum["D"] = "D";
Enum[Enum["E"] = 8] = "E";
Enum[Enum["F"] = 9] = "F";
})(Enum || (Enum = {}));
复制代码
通过观察上述生成的 ES5 代码,我们可以发现数字枚举相对字符串枚举多了 “反向映射”:
console.log(Enum.A) //输出:0
console.log(Enum[0]) // 输出:A
泛型
在计算机科学中,许多算法和数据结构并不会依赖于对象的实际类型。但是,你仍然会想在每个变量里强制提供约束。例如:在一个函数中,它接受一个列表,并且返回这个列表的反向排序,这里的约束是指传入至函数的参数与函数的返回值:
function reverse<T>(items: T[]): T[] {
const toreturn = [];
for (let i = items.length - 1; i >= 0; i--) {
toreturn.push(items[i]);
}
return toreturn;
}
const sample = [1, 2, 3];
let reversed = reverse(sample);
console.log(reversed); // 3, 2, 1
// Safety
reversed[0] = '1'; // Error
reversed = ['1', '2']; // Error
reversed[0] = 1; // ok
reversed = [1, 2]; // ok
在上个例子中,函数 reverse
接受一个类型为 T
(注意在 reverse<T>
中的类型参数) 的数组(items: T[]
),返回值为类型 T 的一个数组(注意:T[]),函数 reverse
的返回值类型与它接受的参数的类型一样。当你传入 const sample = [1, 2, 3]
时,TypeScript 能推断出 reverse
为 number[]
类型,从而能给你类型安全。与此相似,当你传入一个类型为 string[]
类型的数组时,TypeScript 能推断 reverse
为 string
[] 类型,如下例子所示:
const strArr = ['1', '2'];
let reversedStrs = reverse(strArr);
reversedStrs = [1, 2]; // Error
事实上,JavaScript 数组已经拥有了 reverse
的方法,TypeScript 也确实使用了泛型来定义其结构:
interface Array<T> {
reverse(): T[];
}
这意味着,当你在数组上调用 .reverse
方法时,将会获得类型安全:
let numArr = [1, 2];
let reversedNums = numArr.reverse();
reversedNums = ['1', '2']; // Error
当稍后在 环境声明 章节中提及 lib.d.ts
时,我们会讨论更多关于 Array<T>
的信息。
联合类型
在 JavaScript 中,你可能希望属性为多种类型之一,如字符串或者数组。这正是 TypeScript 中联合类型能派上用场的地方(它使用 |
作为标记,如 string | number
)。关于联合类型,一个常见的用例是一个可以接受字符串数组或单个字符串的函数:
function formatCommandline(command: string[] | string) {
let line = '';
if (typeof command === 'string') {
line = command.trim();
} else {
line = command.join(' ').trim();
}
// Do stuff with line: string
}
交叉类型
在 JavaScript 中, extend
是一种非常常见的模式,在这种模式中,你可以从两个对象中创建一个新对象,新对象拥有着两个对象所有的功能。交叉类型可以让你安全的使用此种模式:
function extend<T extends object, U extends object>(first: T, second: U): T & U {
const result = <T & U>{};
for (let id in first) {
(<T>result)[id] = first[id];
}
for (let id in second) {
if (!result.hasOwnProperty(id)) {
(<U>result)[id] = second[id];
}
}
return result;
}
const x = extend({ a: 'hello' }, { b: 42 });
// 现在 x 拥有了 a 属性与 b 属性
const a = x.a;
const b = x.b;
元组类型
JavaScript 并不支持元组,开发者们通常只能使用数组来表示元组。而 TypeScript 支持它,开发者可以使用 :[typeofmember1, typeofmember2]
的形式,为元组添加类型注解,元组可以包含任意数量的成员,示例:
let nameNumber: [string, number];
// Ok
nameNumber = ['Jenny', 221345];
// Error
nameNumber = ['Jenny', '221345'];
将其与 TypeScript 中的解构一起使用:
let nameNumber: [string, number];
nameNumber = ['Jenny', 322134];
const [name, num] = nameNumber;
类型别名
TypeScript 提供了为类型注解设置别名的便捷语法,你可以使用 type SomeName = someValidTypeAnnotation
来创建别名:
type StrOrNum = string | number;
// 使用
let sample: StrOrNum;
sample = 123;
sample = '123';
// 会检查类型
sample = true; // Error
与接口不同,你可以为任意的类型注解提供类型别名(在联合类型和交叉类型中比较实用),下面是一些能让你熟悉类型别名语法的示例。
type Text = string | { text: string };
type Coordinates = [number, number];
type Callback = (data: string) => void;
TIP
- 如果你需要使用类型注解的层次结构,请使用接口。它能使用
implements
和extends
- 为一个简单的对象类型(如上面例子中的 Coordinates)使用类型别名,只需要给它一个语义化的名字即可。另外,当你想给联合类型和交叉类型提供一个语义化的名称时,一个类型别名将会是一个好的选择。
对类型的使用和操作
使用 declare 和 interface or type 就是分别在两个空间编程。比如 Typescript 的泛型就是在类型空间编程,叫做类型编程。除了泛型,还有集合运算,一些操作符比如 keyof 等。值的编程在 Typescript 中更多的体现是在类似 lib.d.ts 这样的库。当然 lib.d.ts 也会在类型空间定义各种内置类型。
lib.d.ts 的内容主要是一些变量声明(如:window、document、math)和一些类似的接口声明(如:Window、Document、Math)。寻找代码类型(如:Math.floor)的最简单方式是使用 IDE 的 F12(跳转到定义)
类型是如何做到静态类型检查的?
TypeScript 要想解决 JavaScript 动态语言类型太宽松的问题,就需要:
- 提供给变量设定类型的能力
注意是变量,不是值。
- 提供常用类型(不必须,但是没有用户体验会极差)并可以扩展出自定义类型(必须)。
- 根据第一步给变量设定的类型进行类型检查,即不允许类型不兼容的赋值, 不允许使用值空间和类型空间不存在的变量和类型等。
第一个点是通过类型注解的语法来完成。即类似这样:
const a: number = 1;
Typescript 的类型注解是这样, Java 的类型注解是另一个样子,Java 类似 int a = 1。 这个只是语法差异而已,作用是一样的。
第二个问题, Typescript 提供了诸如 lib.d.ts 等类型库文件。随着 ES 的不断更新, JavaScript 类型和全局变量会逐渐变多。Typescript 也是采用这种 lib 的方式来解决的。
(TypeScript 提供的部分 lib)
第三个问题,Typescript 主要是通过 interface,type,函数类型等打通类型空间,通过 declare 等打通值空间,并结合 binder 来进行类型诊断。关于 checker ,binder 是如何运作的,自己查找。
类型系统的主要功能
- 定义类型以及其上的属性和方法。
比如定义 String 类型, 以及其原型上的方法和属性。
length, includes 以及 toString 是 String 的成员变量, 生活在值空间, 值空间虽然不能直接和类型空间接触,但是类型空间可以作用在值空间,从而给其添加类型(如上图黄色部分)。
- 提供自定义类型的能力
interface User {
name: string;
age: number;
say(name: string): string;
}
自定义的类型 User,这是 Typescript 必须提供的能力。
- 类型兼容体系。
这个主要是用来判断类型是否正确的.
2.类型推导
有时候你不需要显式说明类型(类型注解),Typescript 也能知道他的类型,这就是类型推导结果。
const a = 1;
如上代码,编译器会自动推导出 a 的类型 为 number。还可以有连锁推导,泛型的入参(泛型的入参是类型)推导等。类型推导还有一个特别有用的地方,就是用到类型收敛。
类型推导和类型收敛
let a = 1;
如上代码。 Typescript 会推导出 a 的类型为 number。
如果只会你这么写就会报错:
a = "1";
因此 string 类型的值不能赋值给 number 类型的变量。我们可以使用 Typescript 内置的 typeof 关键字来证明一下。
let a = 1;
type A = typeof a;
此时 A 的类型就是 number,证明了变量 a 的类型确实被隐式推导成了 number 类型。
有意思的是如果 a 使用 const 声明,a就变为常数了,这就是类型收敛。
const a = 1;
type A = typeof a;
实际情况的类型推导和类型收敛要远比这个复杂, 但是做的事情都是一致的。
比如这个:
function test(a: number, b: number) {
return a + b;
}
type A = ReturnType<typeof test>;
A 就是 number 类型。 也就是 Typescript 知道两个 number 相加结果也是一个 number。因此即使你不显示地注明返回值是 number, Typescript 也能猜到。这也是为什么 JavaScript 项目不接入 Typescript 也可以获得类型提示的原因之一。
除了 const 可以收缩类型, typeof, instanceof 都也可以。 原因很简单,就是Typescript 在这个时候可以 100% 确定你的类型了。 我来解释一下:
比如上面的 const ,由于你是用 const 声明的,因此 100% 不会变,一定永远是 1,因此类型可以收缩为 1。 再比如:
let a: number | string = 1;
a = "1";
if (typeof a === "string") {
a.includes;
}
if 语句内 a 100% 是 string ,不能是 number。因此 if 语句内类型会被收缩为 string。instanceof 也是类似,原理一模一样。只要记住Typescript 如果可以 100% 确定你的类型,并且这个类型要比你定义的或者 Typescript 自动推导的范围更小,那么就会发生类型收缩就行了。
类型断言
// 定义一个空对象,然后给空对象添加一个button属性
let dy = {};
dy.button = 'no btn'; // 报错: 类型“{}”上不存在属性“button”。
// 我们看到报错了,
// 那么怎么解决这个报错呢,这时候我们可以写一个接口,让接口有一个button属性,然后使用断言改写上面的代码
interface Dy {
button:string
}
let dy = {} as Dy
dy.button = 'no btn';
// 但是这样写又一个弊端,不能束缚dy里面一定存在button,
// 所以在不确定的情况下,少用这种断言,那么我们要怎么写呢,在声明的时候就指定类型
let dy:Dy = {button:''}; // 确保dy里面一定受Dy的束缚
dy.button = 'no btn';
类型断言有两种形式:
1.“尖括号” 语法
let someValue: any = "this is a string";
let strLength: number = (<string>someValue).length;
2.as 语法
let someValue: any = "this is a string";
let strLength: number = (someValue as string).length;
类型兼容
结构之间的兼容: 成员少的兼容成员多的 函数之间的兼容:参数多的兼容参数少的
什么是类型兼容? 当一个类型y可以被赋值给类型x时,我们可以说类型x兼容类型y x兼容y: x(目标类型) = y(源类型)
前面只要学,基础的都会掌握,之后就是进入实战,可以现在B站找个视频学一下加深记忆。
前面的掌握后,剩下的可以去React/Vue源码中学习。
参考资料:
概览 | 深入理解 TypeScript (jkchao.github.io)
深入理解 TypeScript | 深入理解 TypeScript (jkchao.github.io)
深入理解TypeScript-Basarat Ali Syed-微信读书 (qq.com)