文章目录
TypeScript 简介
-
TypeScript 是 JavaScript 的一个超集,支持 ECMAScript 6 标准(ES6 教程)。
-
TypeScript 由微软开发的自由和开源的编程语言。
-
TypeScript 设计目标是开发大型应用,它可以编译成纯 JavaScript,编译出来的 JavaScript 可以运行在任何浏览器上。
JavaScript 与 TypeScript 的区别
TypeScript 是 JavaScript 的超集,扩展了 JavaScript 的语法,因此现有的 JavaScript 代码可与 TypeScript 一起工作无需任何修改,TypeScript 通过类型注解提供编译时的静态类型检查。
TypeScript 可处理已有的 JavaScript 代码,并只对其中的 TypeScript 代码进行编译。
TypeScript 优缺点
优点
1. 可维护性强
- 类型系统实际上是最好的文档,大部分的函数看看类型的定义就可以知道如何使用了
- 可以在编译阶段就发现大部分错误,这总比在运行时候出错好
- 增强了编辑器和 IDE 的功能,包括代码补全、接口提示、跳转到定义、重构等
2. 包容性强
- TypeScript 是 JavaScript 的超集,
.js
文件可以直接重命名为.ts
即可 - 即使不显式的定义类型,也能够自动做出类型推论
- 可以定义从简单到复杂的几乎一切类型
- 即使 TypeScript 编译报错,也可以生成 JavaScript 文件
- 兼容第三方库,即使第三方库不是用 TypeScript 写的,也可以编写单独的类型文件供 TypeScript 读取
3. 社区活跃
- 大部分第三方库都有提供给 TypeScript 的类型定义文件
- Google 开发的 Angular2 就是使用 TypeScript 编写的
- TypeScript 拥抱了 ES6 规范,也支持部分 ESNext 草案的规范
缺点
任何事物都是有两面性的,我认为 TypeScript 的弊端在于:
- 有一定的学习成本,需要理解 接口(Interfaces)、泛型(Generics)、类(Classes)、枚举类型(Enums)等前端工程师可能不是很熟悉的概念
- 短期可能会增加一些开发成本,毕竟要多写一些类型的定义,不过对于一个需要长期维护的项目,TypeScript 能够减少其维护成本
- 集成到构建流程需要一些工作量
- 可能和一些库结合的不是很完美
安装
通过 npm 全局安装:npm install -g typescript
通过 yarn 全局安装:yarn global add typescript
查看版本号:tsc -v
TypeScript 在全局安装后,我们可以在任意位置使用 tsc
命令,tsc
命令负责编译 TypeScript 文件为 JavaScript 文件。
TypeScript 基础类型
1. 数字类型(Number)
和 JavaScript 一样,TypeScript 里的所有数字都是浮点数。 这些浮点数的类型是 number
。
除了支持十进制和十六进制字面量,TypeScript 还支持 ECMAScript 2015 中引入的二进制和八进制字面量。
let binaryLiteral: number = 0b1010; // 二进制
let octalLiteral: number = 0o744; // 八进制
let decLiteral: number = 6; // 十进制
let hexLiteral: number = 0xf00d; // 十六进制
2. 字符串类型(String)
使用 string
表示文本数据类型,使用单引号(')或双引号(")来表示字符串类型。反引号(`)来定义多行文本和内嵌表达式
let name: string = "blog";
let years: number = 5;
let words: string = `您好,今年是 ${ name } 发布 ${ years + 1} 周年`;
3. 布尔类型(Boolean)
最基本的数据类型就是简单的 true/false 值,在JavaScript 和 TypeScript 里叫做 boolean
let flag: boolean = true;
4. 数组类型(Array)
TypeScript 像 JavaScript 一样可以操作数组元素。 有两种方式可以定义数组:
// 在元素类型后面加上[]
let arr: number[] = [1, 2];
// 或者使用数组泛型
let arr: Array<number> = [1, 2];
5. 函数类型
// 在函数上直接声名 参数any 和 返回值boolean
function isFalsy(val: any): boolean {
return val === 0 ? false : !val;
}
// 函数没有返回值则使用void
function hello(str: string): void {
alert(str);
}
6. 任意类型(any)
在 TypeScript 中,任何类型都可以被归为 any 类型。这让 any 类型成为了类型系统的顶级类型(也被称作全局超级类型)。
声明为 any
的变量可以赋予任意类型的值,意味着不做任何类型检查,失去TS的意义 (尽量避免使用)
一般使用场景: 第三方库没有提供类型文件时可以使用 any 类型转换遇到困难或者数据结构太复杂难以定义
let x: any = 1; // 数字类型
x = 'hello'; // 字符串类型
x = false; // 布尔类型
如果我们使用 any 类型,就无法使用 TypeScript 提供的大量的保护机制。
为了解决 any 带来的问题,TypeScript 3.0 引入了 unknown 类型。
7. unknown 类型
就像所有类型都可以赋值给 any,所有类型也都可以赋值给 unknown。
这使得 unknown 成为 TypeScript 类型系统的另一种顶级类型(另一种是 any)。
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
但是,当我们尝试将类型为 unknown 的值赋值给其他类型的变量时会发生什么?
let value: unknown;
let value1: unknown = value; // OK
let value2: any = value; // OK
let value3: boolean = value; // Error
let value4: number = value; // Error
let value5: string = value; // Error
let value6: object = value; // Error
let value7: any[] = value; // Error
let value8: Function = value; // Error
unknown 类型只能被赋值给 any 类型和 unknown 类型本身。 只有能够保存任意类型值的容器才能保存 unknown 类型的值。
let value: unknown;
value.foo.bar; // Error
value.trim(); // Error
value(); // Error
new value(); // Error
value[0][1]; // Error
毕竟我们不知道unknown变量 value 中存储了什么类型的值。
8. void类型
用于标识方法返回值的类型,表示该方法没有返回值或返回 undefined
某种程度上来说,void 类型像是与 any 类型相反,它表示没有任何类型。
当一个函数没有返回值时,你通常会见到其返回值类型是 void:
function warnUser(): void {
console.log("This is my warning message");
}
需要注意的是,声明一个 void 类型的变量没有什么作用,因为它的值只能为 undefined 或 null:
let unusable: void = undefined;
9. null 和 undefined类型
null
-
在 JavaScript 中 null 表示 “什么都没有”
-
null是一个只有一个值的特殊类型。表示一个空对象引用
-
用 typeof 检测 null 返回是 object
undefined
-
在 JavaScript 中, undefined 是一个没有设置值的变量。
-
typeof 一个没有值的变量会返回 undefined。
Null 和 Undefined 是所有类型(包括 void)的子类型,可以赋值给其它类型,如数字类型,此时,赋值后的类型会变成 null 或 undefined。
let x: number;
x = 1; // 运行正确
x = undefined; // 运行错误
x = null; // 运行错误
上面的例子中变量 x 只能是数字类型。如果一个类型可能出现 null 或 undefined, 可以用 |
来支持多种类型,示例代码如下:
let x: number | null | undefined;
x = 1; // 运行正确
x = undefined; // 运行正确
x = null; // 运行正确
10. object
object表示非原始类型,也就是除 number,string,boolean,symbol,null 或 undefined 之外的类型
function hello(obj: object | null): void {
console.log(obj);
}
hello({ name: 'zhangsan' });
hello(null);
11. 元组类型(tuple)
元组(Tuple)是一种特殊的数组,它允许你定义数组中每个元素的类型。
与常规数组不同,元组中的元素可以是不同类型的。这提供了一种方式来存储不同类型的数据作为一个有序列表。
使用元组时:
- 必须按照类型依次初始化变量
- 必须提供每个属性的值
let x: [string, number];
x = ['hello', 10]; // OK
x = [10, 'hello']; // Error 必须按照类型依次初始化变量
x = [10]; // Error 必须提供每个属性的值
12. 枚举类型(Enum)
enum
类型是对JavaScript标准数据类型的一个补充,是一种特殊的类型,用于定义一组具名的常量。
使用枚举我们可以很好的描述一些特定的业务场景,比如一年中的春、夏、秋、冬,还有每周的周一到周天,还有各种颜色,以及可以用它来描述一些状态信息,比如错误码等
-
普通枚举(数字枚举)
初始值默认为 0 其余的成员会会按顺序自动增长 可以理解为数组下标enum Color { RED, PINK, BLUE, } const pink: Color = Color.PINK; console.log(pink); // 1
-
设置初始值
enum Color { RED = 10, PINK, BLUE, } const pink: Color = Color.PINK; console.log(pink); // 11
-
字符串枚举
在 TypeScript 2.4 版本,允许我们使用字符串枚举。
在一个字符串枚举里,每个成员都必须用字符串字面量,或另外一个字符串枚举成员进行初始化。enum Color { RED = "红色", PINK = "粉色", BLUE = "蓝色", } const pink: Color = Color.PINK; console.log(pink); // 粉色
-
异构枚举
异构枚举的成员值是数字和字符串的混合enum Color { RED, PINK = "粉色", BLUE = "蓝色", } const red: Color = Color.RED; console.log(red); // 0 console.log(Color[0]); // RED
-
常量枚举
它是使用 const 关键字修饰的枚举,常量枚举与普通枚举的区别是,整个枚举会在编译阶段被删除 我们可以看下编译之后的效果const enum Color { RED, PINK, BLUE, } const color: Color[] = [Color.RED, Color.PINK, Color.BLUE]; //编译之后的js如下: var color = [0 /* RED */, 1 /* PINK */, 2 /* BLUE */]; // 可以看到我们的枚举并没有被编译成js代码 只是把color这个数组变量编译出来了
12. never类型
never
代表永不存在的值(从不会出现的值)
这意味着声明为 never 类型的变量只能被 never 类型所赋值
例如,never 类型是那些总是会抛出异常或根本就不会有返回值的函数表达式或箭头函数表达式的返回值类型。
let x: never;
let y: number;
// 运行错误,数字类型不能转为 never 类型
x = 123;
// 运行正确,never 类型可以赋值给 never类型
x = (()=>{ throw new Error('exception')})();
// 运行正确,never 类型可以赋值给 数字类型
y = (()=>{ throw new Error('exception')})();
// 返回值为 never 的函数可以是抛出异常的情况
function error(message: string): never {
throw new Error(message);
}
// 返回值为 never 的函数可以是无法被执行到的终止点的情况
function loop(): never {
while (true) {}
}
类型推论
理论上来说在我们声明任何变量的时候都需要声明类型(包括普通变量、函数、组件、react hook 等),声明函数、组件、react hook 等需要声明参数和返回值的类型
但是在很多情况下,TS可以帮助我们自动推断,我们就不用声明了,比如:
1. 变量的类型推论
这里虽然没有显示声明,但是ts自动推断这个变量是 string 类型:
可以看到,定义变量 str 时并没有指定它的类型,而是直接赋值一个字符串,当再给它赋一个数值时就会报错。这里 typescript 就根据我们赋给 str 的值的类型,推断出我们的 str 的类型,是字符串类型,所以不可以将数值类型赋给它。
这个就是最基本的类型推论,根据右侧的值推断左侧变量的类型。
2. 返回值的类型推论
ts自动推断函数返回值这是个 number 类型:
3. 多类型联合
当我们定义一个数组或者元组这种包含多个元素的值的时候,多个元素可以有不同的类型,这时候 typescript 会将多个类型合并起来,组成一个联合类型,例如:
const arr = [1, 'a']
arr.push(false) // error,类型“false”的参数不能赋给类型“string | number”的参数
此时的 arr 的元素被推断为 string | number,也就是元素可以是 string 类型也可以是 number 类型,除此之外的类型是不可以的。
再一个例子:
let value = Math.random() * 10 > 5 ? 123 : 'abc'
value = false // error,不能将类型“false”分配给类型“string | number”。
value 的值是随机的,但是只能是 string 或者 number,它的类型被推断出是 string | number,所以不能赋值 false。
4. 上下文类型
前面讲的例子都是根据 = 符号右边值的类型,推断出左侧变量的类型。现在还有一种是根据左侧的类型推断右侧的类型,这就是上下文类型。官网的例子:
window.onmousedown = function (mouseEvent) {
console.log(mouseEvent.abc); // error,mouseEvent 上不存在属性 abc
};
表达式左侧是 window.onmousedown
(鼠标按下时发生事件),因此 TypeScript 会推断赋值表达式右侧函数的参数是事件对象,且是 MouseEvent。在回调函数中使用 mouseEvent 的时候,你可以访问鼠标事件对象的所有属性和方法,当访问不存在属性的时候,就会报错。
类型断言
断言是一种告诉 TypeScript 编译器你确信某个表达式或值的类型是什么的方式。通常这会发生在你清楚地知道一个实体具有比它现有类型更确切的类型。这可以帮助你避免类型错误,尤其是在处理来自第三方库或 JavaScript 代码(没有类型信息)的值时。
使用 <Type>
语法或 as Type
语法。
let someValue: any = "this is a string";
let strLength: number = (<string>someValue).length;
// 或者
let strLength: number = (someValue as string).length;
在上面的例子中,someValue 的类型是 any,但我们确信它是一个字符串,所以我们使用类型断言将其转换为 string 类型,然后访问其 length 属性。
非空断言操作符
使用 !
后缀。这告诉 TypeScript 编译器,你确信某个可能为 null 或 undefined 的值在当前上下文中实际上不是 null 或 undefined。
function fetchData(): string | null {
// ... some logic to fetch data ...
return null; // or a string
}
let data = fetchData();
if (data) {
console.log(data.toUpperCase()); // Error: Object is possibly 'null'.
}
// 使用非空断言操作符
if (data) {
console.log(data!.toUpperCase()); // No error
}
尽管断言在 TypeScript 中很有用,但它们应该谨慎使用。最好只在你确定你的断言是正确的情况下使用它们,因为错误的断言可能会导致运行时错误。
类型守卫
在 TypeScript 中,类型守卫是一种特殊的表达式,它执行运行时检查来缩小一个值的可能类型范围。
类型守卫通常是返回布尔值的函数或方法,它们用于更精确地控制 TypeScript 的类型推断。当 TypeScript 编译器遇到一个类型守卫时,它会根据守卫的结果来更新变量的类型。
in 关键字
in 关键字在 TypeScript 中用于检查一个对象是否包含某个特定的属性。当我们想要确定一个对象是否拥有某个属性时,可以使用 in 关键字。结合类型守卫,我们可以利用 in 关键字来判断一个对象属于哪种类型。
假设我们有一个联合类型 Shape,它可以是 Square 或 Circle。每个类型都有自己的属性:Square 有 size 属性,而 Circle 有 radius 属性。
type Square = { kind: "square"; size: number };
type Circle = { kind: "circle"; radius: number };
type Shape = Square | Circle;
我们可以使用 in 关键字来编写一个类型守卫,以检查 Shape 对象是否具有 radius 属性,从而确定它是否是 Circle 类型:
function isCircle(shape: Shape): shape is Circle {
return "radius" in shape;
}
在这个类型守卫中,"radius" in shape
检查 shape 对象是否包含 radius 属性。如果包含,则函数返回 true,表示 shape 是一个 Circle 类型;否则返回 false。
if (isCircle(shape)) {
// TypeScript 知道 shape 是 Circle 类型,因此可以安全访问 radius
console.log(shape.radius);
} else {
// 处理非 Circle 类型的情况
}
在 if 语句中,如果 isCircle(shape) 返回 true,TypeScript 编译器就知道 shape 是一个 Circle,并允许你访问 radius 属性。
如果没有 shape is Circle 这样的类型断言,TypeScript 将不会允许你访问 radius 属性,因为它只知道 shape 是一个 Shape,它可能是 Square 或 Circle。
通过使用 in 关键字作为类型守卫,我们能够在不引发运行时错误的情况下,安全地处理不同类型的对象,并访问它们的属性。这有助于增强 TypeScript 代码的类型安全性。
.
typeof 关键字
typeof 关键字通常用于获取一个变量或表达式的类型。然而,在类型守卫的上下文中,typeof 也可以用来缩小变量的类型范围。当与特定的类型进行比较时,typeof 可以作为一个类型守卫。
function isNumber(x: any): x is number {
return typeof x === "number";
}
function isString(x: any): x is string {
return typeof x === "string";
}
function example(value: any) {
if (isNumber(value)) {
// TypeScript 知道 value 是 number 类型
console.log(value * 2);
} else if (isString(value)) {
// TypeScript 知道 value 是 string 类型
console.log(value.toUpperCase());
} else {
// 处理其他类型
console.log("Unknown type");
}
}
在这个例子中,isNumber 和 isString 函数都使用 typeof 运算符来检查传入的值是否为 number 或 string 类型,并返回相应的布尔值。通过指定返回类型为 x is number 或 x is string,我们告诉 TypeScript 编译器:如果函数返回 true,那么变量 x 的类型就是我们所断言的类型。
在 example 函数中,我们可以安全地使用不同的操作,因为 TypeScript 编译器会根据类型守卫的结果来推断 value 的类型。如果 value 是一个数字,我们可以安全地乘以 2;如果它是一个字符串,我们可以调用 toUpperCase 方法。
.
instanceof 关键字
instanceof 关键字是一个常用的类型守卫,用于检查一个对象是否是一个构造函数的实例。instanceof 操作符会检查对象是否在其原型链中的某个位置具有给定的构造函数 prototype 属性。
class Animal {
constructor(public name: string) {}
}
class Dog extends Animal {
constructor(name: string) {
super(name);
}
bark() {
console.log(this.name + ' barks!');
}
}
function isDog(animal: Animal | null): animal is Dog {
return animal instanceof Dog;
}
function interactWithAnimal(animal: Animal | null) {
if (animal) {
if (isDog(animal)) {
// TypeScript 知道 animal 是 Dog 类型
animal.bark();
} else {
// 处理其他 Animal 类型或未知类型
console.log(animal.name + ' makes a sound');
}
} else {
console.log('No animal to interact with.');
}
}
const myDog = new Dog('Fido');
const myAnimal = new Animal('Unknown');
interactWithAnimal(myDog); // 输出: Fido barks!
interactWithAnimal(myAnimal); // 输出: Unknown makes a sound
isDog 函数是一个类型守卫,它使用 instanceof 来检查传入的 animal 是否是 Dog 的实例。如果是,则函数返回 true,并告诉 TypeScript 编译器 animal 是一个 Dog 类型的对象。
在 interactWithAnimal 函数中,我们首先检查 animal 是否为 null,然后使用 isDog 类型守卫来确定它的确切类型。如果 animal 是 Dog 类型的,我们就可以安全地调用 bark 方法;否则,我们执行一些默认的操作。
TypeScript type类型别名
TypeScript interface 接口
TypeScript泛型
type 与 interface
type 和 interface 在 TypeScript 中都是用来定义类型的方式,但它们之间存在一些重要的区别,并且适用于不同的场景。
-
语法和定义方式
-
interface 使用 interface 关键字定义,并通过扩展(extends)其他接口或声明合并(declaration merging)来扩展已有的接口。
-
type 使用 type 关键字定义,可以创建类型别名,基于其他类型创建新的类型,并支持更复杂的类型操作,如联合类型、交叉类型等。
-
-
兼容性检查
-
interface 会进行属性的兼容性检查。当一个类型声明实现了某个接口时,TypeScript 会检查该类型是否具有接口中定义的所有属性和方法。
-
type 并不会进行严格的兼容性检查,它只是简单地将类型进行替换。
-
-
可读性和可维护性
-
interface 更加直观和可读性强,适合用于描述对象的形状和行为。
-
type 更加灵活,可以用于定义各种类型别名,适合用于组合和处理多个类型。
-
-
使用场景
-
如果你只是想定义一个对象的形状,并且这个对象可能由多个类或其他接口实现,那么 interface 可能是一个更好的选择。
-
如果你需要创建更复杂的类型,如联合类型、交叉类型、映射类型等,或者你想为现有的类型创建别名,那么 type 可能是更合适的选择。
-