Typescript
参考文章:了不起的 TypeScript 入门教程(1.2W字) - 知乎
介绍
TypeScript 是 JavaScript 的超集。换句话说,JavaScript + 更多功能 = TypeScript。此外,TypeScript还可以编译为纯 JavaScript。
为什么需要TypeScript呢?
主要因为JavaScript 无法满足面向对象编程语言的期望,原因如下:
- 原型继承: JavaScript 使用基于原型的继承模型,而不是传统的类继承模型。这意味着它与许多程序员所熟悉的类和继承的语义不同。
- 缺乏访问修饰符: JavaScript 没有像 Java 那样的访问修饰符(如 public、private、protected)。
- 静态成员和静态方法: JavaScript与具有显式静态关键字的语言相比,显得不够清晰。
- 多重继承: JavaScript 不支持多重继承
- 构造函数和 new 关键字: JavaScript与一些其他语言中的构造函数行为不同,可能会导致混淆。
- 类型系统: JavaScript 是一种弱类型语言,可能导致在开发过程中难以捕捉类型相关的错误。
TypeScript 在很大程度上弥补了 JavaScript 在面向对象编程方面的某些缺陷。
- 静态类型系统: TypeScript 提供了静态类型检查,有助于提高代码的可靠性和维护性。
- 类和继承: TypeScript 引入了 class 关键字,使得定义类和实现继承更加直观和易于理解。
- 访问修饰符: TypeScript 支持 public、private 和 protected 等访问修饰符,这些修饰符提供了对类成员的访问控制,有助于封装和信息的隐藏。
- 接口和抽象类: TypeScript 允许使用接口(interface)来定义对象的形状,以及使用抽象类(abstract class)来定义抽象方法和属性,这有助于创建更加模块化和可复用的代码。
- 泛型: TypeScript 的泛型(generics)允许在保证类型安全的同时创建可重用的组件。
- 装饰器: TypeScript 支持装饰器(decorators),这是一种特殊种类的声明,它可以被附加到类声明、方法、访问符、属性或参数上。装饰器提供了强大的元编程能力,允许开发者扩展类和对象的行为。
如何使用TypeScript
TypeScript可以在 Node.Js 或大部分浏览器上执行,你也可以在vue等框架中使用它。
- Node.js
Node.js安装 TypeScript
$ npm install -g typescript
$ npx tsc --init
编写一个test.ts
const test1:number =1;
console.log(test1);
编译 TypeScript 文件
$ tsc test.ts # 编译ts文件为js
$ node test.js # 执行js
注意:必须配置好node.js的环境变量并用管理员权限运行vscode,否则tsc指令无法运行
- vue框架在使用vite创建项目时选择ts
由于 TypeScript 是 JavaScript 的增强版本,因此 JavaScript 的所有代码在语法上都是有效的 TypeScript。但是,这并不意味着 TypeScript 编译器可以处理所有 JavaScript
let a = 'a'; a = 1; // throws: error TS2322: Type '1' is not assignable to type 'string'.
- ts在线编译器
TypeScript 类型
类型总览
类型 | 描述 | 举例 |
number | 数字类型 | let age: number = 10; |
string | 字符串类型 | let name: string = "张三"; |
boolean | 布尔类型 | let isDone: boolean = false; |
void | 没有返回值的函数类型 | function greet(): void { console.log("Hello"); } |
null | 空值类型 | let n: null = null; |
undefined | 未定义类型 | let u: undefined = undefined; |
any | 任意类型,关闭类型检查 | let notSure: any = 4; |
unknown | 类型安全的任意类型 | let value: unknown; |
never | 永不存在的值的类型(常用于抛出异常或无限循环的函数返回类型) | function error(message: string): never { throw new Error(message); } |
object | 非原始类型,即除number、string、boolean、symbol、null、undefined之外的类型 | let obj: object = { name: "张三", age: 30 }; |
array | 数组类型 | let list: number[] = [1, 2, 3]; |
tuple | 元组类型,固定数量的不同类型元素组合 | let x: [string, number]; x = ["hello", 10]; |
enum | 枚举类型 | enum Color {Red, Green, Blue}; let c: Color = Color.Green; |
备注:其中 object 包含: undefined 、 bigint 、 symbol(表示唯一的、不可变的数据类型) 、 Array 、 Function 、 Date .....
类型注解
类型注解是 TypeScript 中的一个核心概念,它允许开发者显式地指定变量、函数参数、函数返回值等实体的类型。类型注解提供了代码的静态类型信息,这些信息在编译阶段被 TypeScript 编译器使用,以进行类型检查和错误提示。
在 TypeScript 中,类型注解的语法通常是在变量名或参数名后面使用冒号(:)后跟类型名称。
// 变量类型注解
let username: string;
// 函数参数类型注解
function greet(name: string): void {
console.log("Hello, " + name);
}
// 函数返回值类型注解
function getFavoriteNumber(): number {
return 42;
}
// 对象类型注解
let person: { name: string; age: number };
// 数组类型注解
let list: number[];
// 元组类型注解
let tuple: [string, number];
// 枚举类型注解
enum Color {Red, Green, Blue};
let c: Color = Color.Green;
// 类类型注解
class Car {
constructor(public color: string) {}
}
let car: Car;
// 泛型类型注解
let promise: Promise<string>;
// void 类型注解,表示没有返回值的函数
function log(message: string): void {
console.log(message);
}
类型注解的好处包括:
- 增强代码可读性:通过阅读类型注解,开发者可以快速理解代码中各个部分的预期类型。
- 代码维护性:在大型项目中,类型注解可以帮助维护代码,特别是在重构时,可以确保修改不会引入类型错误。
- 开发效率:类型注解可以提供自动完成、代码导航和文档生成等功能,提高开发效率。
- 类型安全:类型注解允许 TypeScript 编译器在编译阶段捕捉到潜在的类型错误,减少运行时错误。
类型注解是 TypeScript 区别于 JavaScript 的一个重要特性,它使得 TypeScript 成为一个静态类型的语言,而 JavaScript 是动态类型的语言。通过类型注解,TypeScript 提供了类型检查和更多的编译时错误检查,有助于编写更可靠和可维护的代码。
类型推断
TypeScript 的 类型推断
是指 TypeScript 编译器在无法直接确定变量类型时,会根据变量的使用情况自动推断出变量的类型。类型推断可以帮助开发者减少类型注解
的编写,同时保持类型安全。
类型推断是 TypeScript 强大的特性之一,它使得开发者可以编写更少的类型注解,同时保持代码的类型安全。然而,类型推断并不是万能的,有时需要开发者提供明确的类型注解来帮助编译器理解代码的意图。
TypeScript 的类型推断主要分为以下几种情况:
- 基础类型推断:
-
- 当声明变量并初始化时,TypeScript 会根据初始化的值来推断变量的类型。
let x = 42; // 推断为 number
-
- 函数的参数也会根据传入的值进行类型推断。
function greet(name) {
console.log(name); // 参数 name 被推断为 string
}
greet("张三");
- 最佳通用类型推断:
-
- 当初始化数组或元组时,TypeScript 会推断出数组或元组中所有元素的通用类型。
let arr = [1, true, "hello"]; // 推断为 ( number | boolean |string)[]
-
- 如果数组或元组中没有任何元素,TypeScript 会推断为 any[]。
- 上下文类型推断:
-
- 在某些情况下,TypeScript 会根据代码的上下文来推断类型,例如函数表达式、箭头函数、对象的属性访问等。
window.onmousedown = function(mouseEvent) {
console.log(mouseEvent.button); // 参数 mouseEvent 被推断为 MouseEvent
};
- 类型推断中的不确定性和 any:
-
- 如果 TypeScript 无法推断出明确的类型,它可能会将变量的类型推断为 any。这通常发生在没有明确初始化的变量上。
let x; // 推断为 any
- 类型参数的推断:
-
- 当使用泛型函数或类时,TypeScript 也会尝试推断类型参数。
function identity<T>(arg: T): T {
return arg;
}
let output = identity("myString"); // 推断为 string
- 类型推断的优先级:
-
- TypeScript 首先尝试使用明确指定的类型注解。
- 如果没有明确注解,会根据初始化的值进行推断。
- 如果初始化值也没有,会考虑上下文类型。
- 如果上下文类型也不适用,则可能推断为 any 或联合类型。
类型断言
在 TypeScript 中,类型断言是一种告诉编译器一个值的类型比它当前推断的类型更具体的方式。类型断言不会改变运行时的值,它只是在编译阶段告诉 TypeScript 编译器如何处理这个值。
TypeScript 编译器不会检查类型断言的正确性,它会假设开发者已经确保了类型断言的准确性。
例如,如果你错误地将一个 number 类型的值断言为 string 类型,然后在运行时访问它的 length 属性,这将导致运行时错误,因为 number 类型没有 length 属性。
类型断言有两种语法形式:
- 尖括号语法:
let someValue: any = "this is a string";
let strLength: number = (<string>someValue).length;
as
语法:
let someValue: any = "this is a string";
let strLength: number = (someValue as string).length;
在 TypeScript 2.0 及以上版本中,推荐使用 as
语法,因为它不会与 JSX 发生冲突,并且更易于阅读。
类型断言在以下情况下非常有用:
- 当你从 JavaScript 迁移到 TypeScript 时,你可能会遇到一些无法立即提供类型注解的库或第三方代码。在这种情况下,可以使用类型断言来指定更具体的类型。
- 当你正在处理联合类型,并且你确定变量的具体类型时,可以使用类型断言来访问特定类型的属性或方法。
- 当你从宽泛的类型转换到更具体的类型时,类型断言可以帮助 TypeScript 编译器理解你的意图。
常⽤类型
基本类型
对于基本类型,如string、number、boolean、null、undefined、bigint等,
因为TypeScript 很擅长推断类型,所以在某些情况下,你不需要显式地注解类型。例如,当一个变量被立即初始化时,TypeScript 可以根据初始化的值推断出类型。
let a = 114514;
a = "鸡你太美";//不能将类型“string”分配给类型“number”
可选类型:
如果一个属性或参数是可选的,可以在类型后面加上 |
符号。
let optionalName: string | undefined; //optionalName 可能是 string 或 undefined。
字面量:
TypeScript 允许使用字面量类型,这意味着你可以将一个变量限定为一个具体的字面量值。这在某些情况下非常有用,比如限定一个配置对象的特定属性。
let a: '你好' //a的值只能为字符串“你好”
let b: 100 //b的值只能为数字100
a = '欢迎'//警告:不能将类型“"欢迎"”分配给类型“"你好"”
b = 200 //警告:不能将类型“200”分配给类型“100”
let gender: '男'|'⼥' //定义⼀个gender变量,值只能为字符串“男”或“⼥”
gender = '男'
gender = '未知' //不能将类型“"未知"”分配给类型“"男" | "⼥"”
any类型和unknown
在 TypeScript 中,any
和 unknown
类型都用于处理类型不确定的情况,但它们的行为和用途有所不同
any 类型
any
类型是 TypeScript 中的顶级类型,它允许赋值为任何类型,也允许任何类型的操作,包括属性访问、方法调用等。当你不想在编译阶段对值进行类型检查时,可以使用 any
类型。这相当于关闭了 TypeScript 的类型检查功能。
let value: any;
value = true; // OK
value = 42; // OK
value = "hello"; // OK
value = {}; // OK
value = []; // OK
value = null; // OK
value = undefined; // OK
value.myMethod(); // OK (不会检查方法是否存在)
value.myProperty; // OK (不会检查属性是否存在)
unknown 类型
unknown
类型是 TypeScript 3.0 引入的新类型,它也是所有类型的超类型,意味着任何类型的值都可以赋给 unknown
类型的变量。然而,与 any
类型不同的是,unknown
类型在类型检查上是安全的,因为在对 unknown
类型的值进行操作之前,必须进行某种形式的类型检查。
let value: unknown;
value = true; // OK
value = 42; // OK
value = "hello"; // OK
value = {}; // OK
value = []; // OK
value = null; // OK
value = undefined; // OK
// 错误:对象类型“unknown”上不存在属性“myMethod”
// value.myMethod(); // Error
// 错误:对象类型“unknown”上不存在属性“myProperty”
// value.myProperty; // Error
// 正确:使用类型断言来告诉编译器 value 的具体类型
(value as any).myMethod(); // OK (通过 any 类型断言)
(value as any).myProperty; // OK (通过 any 类型断言)
// 或者使用更安全的类型检查
if (typeof value === "object" && value !== null) {
value.myMethod(); // OK (在运行时检查 value 是否有 myMethod 方法)
}
总结
any
类型允许完全绕过类型检查,这可能会导致运行时错误,因为它不会检查属性或方法是否存在。而 unknown
类型要求在操作之前进行类型检查或类型断言,这使得 TypeScript 能够在编译时捕获潜在的错误。因此,推荐尽可能使用 unknown
类型而不是 any
类型,以保持类型安全性。
void和never
- void 的含义是: 空 或 undefined ,严格模式下不能将null 或undefined赋值给void 类型。 void 常⽤于限制函数返回值 。
// ⽆警告
function demo1():void{
}
// ⽆警告
function demo2():void{
return
}
// ⽆警告
function demo3():void{
return undefined
}
//
有警告:不能将类型“number”分配给类型“void”
function demo4():void{
return 666
}
- never 的含义是:任何值都不是,简⾔之就是不能有值, undefined 、 null 、 '' 、 0 都不 ⾏!
-
- ⼏乎不⽤ never 去直接限制变量,因为没有意义
- never ⼀般是 TypeScript 主动推断出来的
- never 也可⽤于限制函数的返回值
let a: string
if(typeof a === 'string'){
a.toUpperCase()
}else{
console.log(a) // TypeScript会推断出此处的a是never,因为没有任何⼀个值符合此处的
逻辑
}
// 限制demo函数不需要有任何返回值
function demo():never{
throw new Error('程序异常退出')
}
Object 与 object
关于 Object 与 object ,直接说结论:在类型限制时, Object ⼏乎不⽤,因为范围太⼤了,⽆意义。
- object 的含义:任何【⾮原始值类型】,包括:对象、函数、数组等,限制的范围⽐较宽泛,⽤ 得少
let a:object //a的值可以是任何【⾮原始值类型】,包括:对象、函数、数组等
//以下代码,是将【⾮原始类型】赋给a,所以均⽆警告
a = {}
a = {name:'张三'}
a = [1,3,5,7,9]
a = function(){}
//以下代码,是将【原始类型】赋给a,有警告
a = null
a = 1
a = true
- Object 的含义: Object 的实例对象,限制的范围太⼤了,⼏乎不⽤。
let a:Object
// 只有以下代码均有警告
a = null
// 警告:不能将类型“null”分配给类型“Object”
a = undefined // 警告:不能将类型“undefined”分配给类型“Object”
- 实际开发中,限制⼀般对象,通常使⽤以下形式
// 限制person对象的具体内容,使⽤【,】分隔,问号代表可选属性
let person: { name: string, age?: number}
// 限制car对象的具体内容,使⽤【;】分隔,必须有price和color属性,其他属性不去限制,有没有都⾏
let car: { price: number; color: string; [k:string]:any}
// 限制student对象的具体内容,使⽤【回⻋】分隔
let student: {
id: string
grade:number
}
// 以下代码均⽆警告
person = {name:'张三',age:18}
person = {name:'李四'}
car = {price:100,color:'红⾊'}
student = {id:'tetqw76te01',grade:3}
- 限制函数的参数、返回值,使⽤以下形式
let demo: (a: number, b: number) => number
demo = function(x,y) {
return x+y
}
- 限制数组,使⽤以下形式
let arr1: string[] // 该⾏代码等价于: let arr1: Array<string>
let arr2: number[] // 该⾏代码等价于:let arr2: Array<number>
arr1 = ['a','b','c']
arr2 = [1,3,5,7,9]
tuple
元组(Tuple)是一种固定数量的元素组成的数组,其中每个元素的类型都可以不同。
基本使用如下:
// 声明元组
let tuple: [string, number];
// 初始化元组
tuple = ["hello", 10]; // 正确
// tuple = [10, "hello"]; // 错误,类型不匹配
// tuple = ["hello"]; // 错误,元素数量不足
//通过索引来访问元组中的元素
console.log(tuple[0]); // 输出 "hello"
console.log(tuple[1]); // 输出 10
//默认情况下,元组不允许添加超出其定义长度的元素
tuple.push(true); // 错误,不能添加布尔值到 [string, number] 类型的元组中
//如果你确实需要添加元素,你可以通过类型断言来绕过这个限制
tuple.push(true as any); // 通过 any 类型断言绕过类型检查
你可以将元组定义为只读的,这意味着一旦创建,就不能更改其元素
let readOnlyTuple: readonly [string, number] = ["hello", 10];
// readOnlyTuple[0] = "world"; // 错误,只读元组不允许修改
解构元组
let [first, second] = tuple;
console.log(first); // 输出 "hello"
console.log(second); // 输出 10
虽然元组在定义时每个元素的类型是确定的,但在运行时,元素的值仍然可以是 undefined 或 null,除非你明确地初始化它们
let undefinedTuple: [string, number];
console.log(undefinedTuple[0]); // 输出 undefined
元组常用于以下场景:
- 函数返回多个值。
- 当你需要一个固定数量的元素集合,且每个元素类型不同。
// 定义一个元组类型
type Person = [string, number, boolean];
// 创建一个 Person 类型的元组
let person: Person = ["张三", 30, true];
// 使用解构来访问元组的元素
let [name, age, isStudent] = person;
console.log(name, age, isStudent); // 输出 "张三", 30, true
枚举
TypeScript 中的枚举(Enum)是一种用于创建命名的常数集合的数据类型。枚举允许开发者定义一组相关的命名常量,使得代码更加清晰和易于维护。TypeScript 支持数字枚举和字符串枚举,以及异构枚举(即包含数字和字符串成员的枚举)。
- 数字枚举
数字枚举是最常见的枚举类型,成员值默认为从 0 开始的递增数字,但也可以手动设置每个成员的值。
enum Direction {
Up, // 默认为 0
Down, // 默认为 1
Left, // 默认为 2
Right, // 默认为 3
}
console.log(Direction.Up); // 输出 0
console.log(Direction.Down); // 输出 1
你也可以手动设置枚举成员的值:
enum Direction {
Up = 1,
Down = 2,
Left = 3,
Right = 4,
}
console.log(Direction.Up); // 输出 1
数字枚举还支持计算成员和常量成员。计算成员是使用表达式得到的值,而常量成员是枚举表达式中的字面量值。
enum Color {
Red,
Green,
Blue = 2 * 2,
Purple = Color.Red + Color.Green,
}
console.log(Color.Purple); // 输出 3 (Red + Green)
- 字符串枚举
字符串枚举的每个成员都必须用字符串字面量或另一个字符串枚举成员进行初始化。
enum FileAccess {
None = "",
Read = "r",
Write = "w",
ReadWrite = "rw",
}
console.log(FileAccess.Read); // 输出 "r"
字符串枚举提供了更好的可读性和稳定性,因为它们在编译时是完全确定的。
- 异构枚举
异构枚举是包含数字和字符串成员的枚举。这种枚举不常见,因为它们可能会导致混淆。
enum Mixed {
Num = 1,
Str = "hello",
}
- 反向映射
数字枚举具有反向映射的特性,即可以从枚举的值映射回枚举的名字。
enum Direction {
Up,
Down,
Left,
Right,
}
console.log(Direction[0]); // 输出 "Up"
字符串枚举没有这个特性。
- 常量枚举
使用const
关键字定义的枚举是常量枚举。它们在编译时会被完全删除,并且不能包含计算成员。常量枚举可以提高性能,因为它们不会增加编译后的代码体积。
const enum Direction {
Up,
Down,
Left,
Right,
}
console.log(Direction.Up); // 输出 0
- 枚举成员的类型
枚举成员也有自己的类型,即枚举本身。这意味着你可以将枚举成员作为类型来使用。
enum ShapeKind {
Circle,
Square,
}
interface Circle {
kind: ShapeKind.Circle;
radius: number;
}
interface Square {
kind: ShapeKind.Square;
sideLength: number;
}
let c: Circle = {
kind: ShapeKind.Circle,
radius: 100,
};
- 使用细节
-
- 枚举成员是双向映射的,数字枚举成员可以映射到名字,名字也可以映射回数字。
- 字符串枚举没有数字枚举的反向映射特性。
- 枚举成员的值必须是唯一的。
- 枚举可以用来创建一组相关的常量,提高代码的可读性和维护性。
- 枚举是开放的,可以随时添加新的成员,但要注意不要改变已有成员的值。
- 枚举成员的类型可以用来限制接口或类型别名的属性。
抽象类
在 TypeScript 中,抽象类(Abstract Class)是一种不能直接实例化的类,它用来作为其他类的基类。抽象类可以包含具体实现的方法,也可以包含抽象方法(没有具体实现的方法,只有签名)。抽象类通常用于定义接口和实现的一部分,让子类去实现剩余的部分。
- 抽象类的定义
定义抽象类使用abstract
关键字。抽象类不能直接实例化,但可以包含构造函数。抽象类中的抽象方法不包含具体实现,只有方法签名。
abstract class Animal {
abstract makeSound(): void;
move(): void {
console.log("Moving along!");
}
}
- 抽象方法
抽象方法是没有实现的方法,它只是定义了方法的签名。子类必须实现所有的抽象方法,否则子类也必须标记为抽象类。
abstract class Animal {
abstract makeSound(): void;
}
- 实现抽象类
子类必须实现基类中的所有抽象方法。子类可以是具体的类,也可以是抽象类。
class Dog extends Animal {
makeSound(): void {
console.log("Woof!");
}
}
class Cat extends Animal {
makeSound(): void {
console.log("Meow!");
}
}
- 使用细节
-
- 抽象类不能直接实例化,只能被继承。
- 抽象类可以包含具体实现的方法和属性。
- 抽象类可以包含抽象方法,这些方法必须在子类中被实现。
- 抽象类可以包含构造函数,子类的构造函数必须调用
super()
。 - 子类可以实现基类中的所有抽象方法,也可以是抽象类,继续将实现的责任传递给更具体的子类。
abstract class Shape {
abstract area(): number;
toString(): string {
return `Shape with area ${this.area()}`;
}
}
class Circle extends Shape {
constructor(private radius: number) {
super();
}
area(): number {
return Math.PI * this.radius * this.radius;
}
}
class Square extends Shape {
constructor(private sideLength: number) {
super();
}
area(): number {
return this.sideLength * this.sideLength;
}
}
let circle = new Circle(5);
console.log(circle.toString()); // Output: "Shape with area 78.53981633974483"
let square = new Square(4);
console.log(square.toString()); // Output: "Shape with area 16"
在这个例子中,Shape
是一个抽象类,它定义了一个抽象方法 area()
。Circle
和 Square
是 Shape
的具体子类,它们实现了 area()
方法。通过这种方式,抽象类提供了一个通用的接口,而具体的实现留给子类来完成。
⾃定义类型
在 TypeScript 中,自定义类型允许开发者创建自己的类型别名或接口,以便于重用和抽象类型。自定义类型可以提高代码的可读性和可维护性,并且可以更好地描述数据的结构和行为。
- 类型别名
类型别名是为一组已存在的类型创建一个新的名称。使用type
关键字来定义类型别名。类型别名可以用于原始类型、联合类型、元组类型等。
type Point = {
x: number;
y: number;
};
let p: Point = { x: 10, y: 20 };
- 接口
接口是一种定义对象类型的结构。接口可以包含属性、方法、索引签名和类类型的继承。
interface IPoint {
x: number;
y: number;
z?: number; // 可选属性
readonly id: string; // 只读属性
[propName: string]: any; // 索引签名,允许对象有任意数量的额外属性。
}
let p: IPoint = { x: 10, y: 20, id: "123" };
- 使用细节
-
- 类型别名和接口都可以用来描述对象的结构,但它们有一些区别。类型别名通常用于简单的类型,而接口更适合于描述复杂的对象类型和类类型。
- 类型别名可以直接用于原始类型、联合类型和元组类型,而接口只能用于对象类型。
- 接口可以扩展其他接口或类,类型别名不能扩展,但可以联合其他类型。
- 接口可以多次定义,类型别名不能重复定义。
- 类型别名和接口都可以被 implements 关键字用于类,以实现特定的类型结构。
// 使用类型别名定义一个函数类型
type AddFunction = (a: number, b: number) => number;
// 使用接口定义一个函数类型
interface IAddFunction {
(a: number, b: number): number;
}
// 使用类型别名定义一个联合类型
type StringOrNumber = string | number;
// 使用接口定义一个具有多个属性的复杂对象
interface IShape {
color: string;
area(): number;
}
// 使用接口定义一个类类型
class Circle implements IShape {
constructor(public color: string, private radius: number) {}
area(): number {
return Math.PI * this.radius * this.radius;
}
}
类型守卫
TypeScript 的类型守卫(Type Guards)是一种在运行时检查变量类型的方式。类型守卫可以确保在代码的执行过程中,变量的类型是特定的,从而避免运行时错误。
类型守卫可以是基于 TypeScript 的类型检查机制的函数,也可以是条件语句。类型守卫的目的是确定一个变量是某种类型,而不是其他类型。
常见的类型守卫方式
- 在
switch
语句中使用具体的类型:
switch (x) {
case x.length:
// x 是一个字符串
break;
default:
// x 不是字符串
}
- 使用
typeof
操作符:
if (typeof x === "string") {
// x 是一个字符串
}
- 使用
instanceof
操作符:
if (x instanceof MyClass) {
// x 是一个 MyClass 的实例
}
- 使用自定义的类型守卫函数:
function isString(value: any): value is string {
return typeof value === "string";
}
if (isString(x)) {
// x 是一个字符串
}
- 使用
in
关键字:
interface MyInterface {
prop: number;
}
function hasProp(obj: any, prop: string): obj is MyInterface {
return obj.hasOwnProperty(prop);
}
if (hasProp(x, "prop")) {
// x 有一个名为 "prop" 的属性
}
细节
- 类型守卫应该在变量类型可能发生变化的地方使用,以避免运行时错误。
- 类型守卫应该尽可能精确,以减少误判的可能性。
- 在使用类型守卫时,应确保守卫的条件在所有相关情况下都成立。
联合类型
在 TypeScript 中,联合类型(Union Types)是一种允许一个变量可以同时是多种类型中的一种的类型。当一个变量声明为联合类型时,你可以通过赋值不同的类型给它,来改变它的类型。联合类型是使用竖线 |
来分隔不同类型的。
定义联合类型
let x: number | string; //x 可以是 number 或 string 类型。
联合类型的使用
x = 10; // 正确
x = "hello"; // 正确
注意事项
- 在使用联合类型的变量时,你不能访问这个变量只有在特定类型下才存在的属性或方法。例如,如果你有一个
number | string
类型的变量,你不能调用.charAt(0)
方法,因为这可能会导致运行时错误。 - 如果你需要访问特定类型的属性或方法,你需要使用类型断言或类型检查来确保你的操作是安全的。
- TypeScript 不会自动在运行时检查联合类型的具体类型,因此你需要确保在代码中使用类型守卫来正确地处理联合类型的变量。
interface Bird {
fly(): void;
}
interface Fish {
swim(): void;
}
class Canary implements Bird {
fly(): void {
console.log("Fly like a canary");
}
}
class Shark implements Fish {
swim(): void {
console.log("Swim like a shark");
}
}
class Dolphin implements Bird, Fish {
fly(): void {
console.log("Fly like a dolphin (?)");
}
swim(): void {
console.log("Swim like a dolphin");
}
}
let pet: Bird | Fish;
pet = new Canary();
pet.fly();
pet = new Shark();
pet.swim();
// 错误:Dolphin 同时实现了 Bird 和 Fish,但是不能确定它是一个 Bird
// pet.fly(); // 编译时错误
// 使用类型断言来访问特定类型的方法
pet = new Dolphin();
(pet as Bird).fly();
(pet as Fish).swim();
在这个示例中,pet
是一个 Bird | Fish
类型的变量。通过类型断言,我们可以安全地调用 fly
或 swim
方法,因为 TypeScript 知道 pet
确实是一个 Bird
或 Fish
。如果没有使用类型断言,我们只能调用 fly
或 swim
中的一个,以确保类型安全。
交叉类型
在 TypeScript 中,交叉类型是一种组合多个类型特征的方式。交叉类型允许你创建一个对象,这个对象同时拥有多个类型的属性和方法。当一个变量声明为交叉类型时,它必须同时满足所有组成交叉类型的类型。
定义交叉类型
交叉类型是通过使用并集操作符 &
来定义的。
interface Bird {
fly(): void;
}
interface Fish {
swim(): void;
}
// 交叉类型
let pet: Bird & Fish; //pet 同时需要实现 Bird 和 Fish 的接口
交叉类型的使用
class Dolphin implements Bird & Fish {
fly(): void {
console.log("Fly like a dolphin (?)");
}
swim(): void {
console.log("Swim like a dolphin");
}
}
let dolphin: Bird & Fish;
dolphin = new Dolphin();
dolphin.fly(); // 调用 Bird 接口的方法
dolphin.swim(); // 调用 Fish 接口的方法
注意事项
- 交叉类型要求对象同时满足所有组成交叉类型的类型。这意味着对象必须同时实现所有类型的属性和方法。
- 交叉类型通常用于创建具有多个类型的对象,例如,当你需要一个对象同时具有两个或更多类型的特征时。
interface Square {
kind: "square";
sideLength: number;
description(): string;
}
interface Rectangle {
kind: "rectangle";
width: number;
height: number;
description(): string;
}
interface Circle {
kind: "circle";
radius: number;
description(): string;
}
// 交叉类型
interface Shape {
kind: "square" | "rectangle" | "circle";
description(): string;
}
function createShape(kind: "square"): Square;
function createShape(kind: "rectangle"): Rectangle;
function createShape(kind: "circle"): Circle;
function createShape(kind: "square" | "rectangle" | "circle"): Shape {
switch (kind) {
case "square":
return { kind: "square", sideLength: 10, description: () => "A square with side length 10" };
case "rectangle":
return { kind: "rectangle", width: 10, height: 20, description: () => "A rectangle with width 10 and height 20" };
case "circle":
return { kind: "circle", radius: 5, description: () => "A circle with radius 5" };
}
}
let shape: Shape;
shape = createShape("square");
console.log(shape.description()); // 输出: "A square with side length 10"
在这个示例中,createShape
函数接受一个字符串参数 kind
,并返回一个具有相应类型的形状对象。通过使用交叉类型,createShape
函数可以创建一个同时具有 Square
、Rectangle
和 Circle
类型的对象。
TypeScript 函数
在 TypeScript 中,函数与 JavaScript 函数非常相似,它们都基于 ES6 的箭头函数语法,并且支持参数、返回值、可选参数、默认参数、剩余参数、命名参数等。然而,TypeScript 在类型检查和代码结构方面提供了更多的支持。
与 JavaScript 函数的区别
- 类型检查:TypeScript 提供了更严格的类型检查,允许你为函数的参数和返回值添加类型注解,这有助于提高代码的可读性和可维护性。
- 重载:JavaScript 本身不支持函数重载,但 TypeScript 提供了这一特性。
- 命名参数:JavaScript 支持通过 arguments 对象访问所有参数,但 TypeScript 提供了命名参数,这有助于提高代码的可读性。
类型检查
在 TypeScript 中,函数的类型检查是通过函数的参数类型和返回值类型来进行的。你可以为函数的参数和返回值添加类型注解,以指示期望的类型。TypeScript 编译器会根据这些类型注解来检查代码,以确保函数的参数和返回值符合指定的类型。
- 参数类型:可以为函数的每个参数添加类型注解,指定参数的期望类型。
- 返回值类型:可以为函数添加返回值类型注解,指定函数应该返回的类型。
- 可选参数和默认参数:TypeScript 支持为函数参数添加可选和默认参数。
- 剩余参数:TypeScript 支持将剩余参数作为数组处理。
- 重载:TypeScript 支持函数重载,允许为同一个函数提供多个签名。
示例:
// 定义一个简单的函数,带有一个参数和一个返回值
function add(x: number, y: number): number {
return x + y;
}
// 定义一个函数,带有一个可选参数和一个默认参数
function multiply(x: number, y: number = 1): number {
return x * y;
}
// 定义一个函数,使用剩余参数作为数组
function sumAll(...numbers: number[]): number {
return numbers.reduce((a, b) => a + b, 0);
}
// 定义一个函数,使用命名参数
function greet(greeting: string, name: string): string {
return greeting + " " + name;
}
// 定义一个函数,使用重载
function reverse(x: string): string;
function reverse(x: number): number;
function reverse(x: any): any {
return x.split("").reverse().join("");
}
- 类型注解提供了编译时的类型检查,但不会影响运行时的行为。
- 函数的类型检查是基于参数和返回值的类型定义进行的。
- 如果函数的参数或返回值类型不匹配,TypeScript 编译器将生成错误。
- 函数重载允许你为同一个函数提供多个签名,但每个签名都必须有不同的参数类型或数量。
- 如果你不提供返回值类型注解,TypeScript 会将返回值推断为
any
类型。 - 箭头函数也可以使用类型注解,与传统函数的类型检查方式相同。
// 定义一个箭头函数,带有一个参数和一个返回值
const add = (x: number, y: number): number => x + y;
// 定义一个箭头函数,带有一个可选参数和一个默认参数
const multiply = (x: number, y: number = 1): number => x * y;
// 定义一个箭头函数,使用剩余参数作为数组
const sumAll = (...numbers: number[]): number => numbers.reduce((a, b) => a + b, 0);
// 定义一个箭头函数,使用命名参数
const greet = (greeting: string, name: string): string => greeting + " " + name;
// 定义一个箭头函数,使用重载
const reverse = (x: string): string => x.split("").reverse().join("");
const reverse = (x: number): number => x.toString().split("").reverse().join("");
重载
在 TypeScript 中,函数重载是一种允许你为同一个函数名定义多个签名的特性。这使得你可以使用同一个函数名来处理不同数量或类型的参数,而编译器会根据调用时的参数来决定使用哪个函数签名。
基本用法
函数重载的语法是在函数声明之前列出多个函数签名,每个签名由参数类型和返回类型组成。
function reverse(x: string): string;
function reverse(x: number): number;
function reverse(x: string | number): string | number {
return x.split("").reverse().join("");
}
重载的调用
根据传递给 reverse
函数的参数类型,TypeScript 会决定使用哪个重载签名。
console.log(reverse("hello")); // 输出 "olleh"
console.log(reverse(123)); // 输出 321
使用细节
- 每个重载签名可以有不同数量的参数或不同类型的参数。
- 重载签名不能有不同的返回类型,但可以有不同的参数类型。
- 函数重载仅在编译阶段有效,在运行时,函数的签名和参数数量必须完全匹配。
- 重载签名必须按照从最具体到最一般的顺序排列,因为编译器会从第一个签名开始匹配,一旦找到匹配的签名,就会使用它,即使后面有更匹配的签名也不会考虑。
// 重载函数,根据参数数量和类型执行不同的操作
function log(message: string): void;
function log(message: string, data: any): string;//错误,重载签名不能有不同的返回类型
function log(message: any, data?: any): void {
if (typeof message === "string") {
console.log(message);
} else {
console.log(`Logging: ${message}`);
}
if (data !== undefined) {
console.log(data);
}
}
log("Hello, world!"); // 调用第一个重载签名
log("Hello, world!", "This is some additional data."); // 调用第二个重载签名
命名参数
在 TypeScript 中,命名参数是一种为函数参数提供名称的语法,这有助于提高代码的可读性和可维护性。命名参数允许你按照参数的顺序和名称来明确地调用函数,这在你有多个参数且需要指定参数时非常有用。
基本用法
function greet(greeting: string, name: string): string {
return greeting + " " + name;
}
// 使用命名参数调用函数
const message = greet(greeting = "Hello", name = "Alice");
使用细节
- 命名参数必须按照参数定义的顺序使用,否则 TypeScript 编译器将报错。
- 命名参数只对函数调用有效,对函数定义没有影响。即使你在函数调用中使用了命名参数,函数定义仍然需要使用传统的方式(即按位置传递参数)。
- 如果你不使用命名参数,但函数定义中使用了命名参数,你仍然需要按照位置传递参数。
- 如果你使用命名参数,但函数定义中没有使用命名参数,你仍然可以按照位置传递参数,或者在调用时省略命名参数,但这样可能会导致代码不够清晰。
// 定义一个使用命名参数的函数
function greet(greeting: string, name: string): string {
return greeting + " " + name;
}
// 使用命名参数调用函数
const message = greet(greeting = "Hello", name = "Alice");
// 或者,使用传统的方式调用函数
const message2 = greet("Hello", "Alice");
TypeScript 类
类的属性与方法
在面向对象语言中,类是一种面向对象计算机编程语言的构造,是创建对象的蓝图,描述了所创建的对象共同的属性和方法。
在 TypeScript 中,我们可以通过 Class 关键字来定义一个类:
class Greeter {
// 静态属性
static cname: string = "Greeter";
// 成员属性
greeting: string;
// 构造函数 - 执行初始化操作
constructor(message: string) {
this.greeting = message;
}
// 静态方法
static getClassName() {
return "Class name is Greeter";
}
// 成员方法
greet() {
return "Hello, " + this.greeting;
}
}
let greeter = new Greeter("world");
那么成员属性与静态属性,成员方法与静态方法有什么区别呢?这里无需过多解释,我们直接看一下以下编译生成的 ES5 代码:
"use strict";
var Greeter = /** @class */ (function () {
// 构造函数 - 执行初始化操作
function Greeter(message) {
this.greeting = message;
}
// 静态方法
Greeter.getClassName = function () {
return "Class name is Greeter";
};
// 成员方法
Greeter.prototype.greet = function () {
return "Hello, " + this.greeting;
};
// 静态属性
Greeter.cname = "Greeter";
return Greeter;
}());
var greeter = new Greeter("world");
访问器
在 TypeScript 中,我们可以通过 getter 和 setter 方法来实现数据的封装和有效性校验,防止出现异常数据。
let passcode = "Hello TypeScript";
class Employee {
private _fullName: string;
get fullName(): string {
return this._fullName;
}
set fullName(newName: string) {
if (passcode && passcode == "Hello TypeScript") {
this._fullName = newName;
} else {
console.log("Error: Unauthorized update of employee!");
}
}
}
let employee = new Employee();
employee.fullName = "Semlinker";
if (employee.fullName) {
console.log(employee.fullName);
}
类的继承
继承 (Inheritance) 是一种联结类与类的层次模型。指的是一个类(称为子类、子接口)继承另外的一个类(称为父类、父接口)的功能,并可以增加它自己的新功能的能力,继承是类与类或者接口与接口之间最常见的关系。
继承是一种 is-a关系:
在 TypeScript 中,我们可以通过 extends 关键字来实现继承:
class Animal {
name: string;
constructor(theName: string) {
this.name = theName;
}
move(distanceInMeters: number = 0) {
console.log(`${this.name} moved ${distanceInMeters}m.`);
}
}
class Snake extends Animal {
constructor(name: string) {
super(name);
}
move(distanceInMeters = 5) {
console.log("Slithering...");
super.move(distanceInMeters);
}
}
let sam = new Snake("Sammy the Python");
sam.move();
ECMAScript 私有字段
在 TypeScript 3.8 版本就开始支持ECMAScript 私有字段,使用方式如下:
class Person {
#name: string;
constructor(name: string) {
this.#name = name;
}
greet() {
console.log(`Hello, my name is ${this.#name}!`);
}
}
let semlinker = new Person("Semlinker");
semlinker.#name;
// ~~~~~
// Property '#name' is not accessible outside class 'Person'
// because it has a private identifier.
与常规属性(甚至使用 private 修饰符声明的属性)不同,私有字段要牢记以下规则:
- 私有字段以 # 字符开头,有时我们称之为私有名称;
- 每个私有字段名称都唯一地限定于其包含的类;
- 不能在私有字段上使用 TypeScript 可访问性修饰符(如 public 或 private);
- 私有字段不能在包含的类之外访问,甚至不能被检测到。
TypeScript 泛型
软件工程中,我们不仅要创建一致的定义良好的 API,同时也要考虑可重用性。 组件不仅能够支持当前的数据类型,同时也能支持未来的数据类型,这在创建大型系统时为你提供了十分灵活的功能。
在像 C# 和 Java 这样的语言中,可以使用泛型来创建可重用的组件,一个组件可以支持多种类型的数据。 这样用户就可以以自己的数据类型来使用组件。
设计泛型的关键目的是在成员之间提供有意义的约束,这些成员可以是:类的实例成员、类的方法、函数参数和函数返回值。
泛型(Generics)是允许同一个函数接受不同类型参数的一种模板。相比于使用 any 类型,使用泛型来创建可复用的组件要更好,因为泛型会保留参数类型。
泛型接口
interface GenericIdentityFn<T> {
(arg: T): T;
}
泛型类
class GenericNumber<T> {
zeroValue: T;
add: (x: T, y: T) => T;
}
let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function (x, y) {
return x + y;
};
泛型变量
对刚接触 TypeScript 泛型的小伙伴来说,看到 T 和 E,还有 K 和 V 这些泛型变量时,估计会一脸懵逼。其实这些大写字母并没有什么本质的区别,只不过是一个约定好的规范而已。也就是说使用大写字母 A-Z 定义的类型变量都属于泛型,把 T 换成 A,也是一样的。下面我们介绍一下一些常见泛型变量代表的意思:
- T(Type):表示一个 TypeScript 类型
- K(Key):表示对象中的键类型
- V(Value):表示对象中的值类型
- E(Element):表示元素类型
泛型工具类型
为了方便开发者 TypeScript 内置了一些常用的工具类型,比如 Partial、Required、Readonly、Record 和 ReturnType 等。出于篇幅考虑,这里我们只简单介绍 Partial 工具类型。不过在具体介绍之前,我们得先介绍一些相关的基础知识,方便读者自行学习其它的工具类型。
1.typeof
在 TypeScript 中,typeof 操作符可以用来获取一个变量声明或对象的类型。
interface Person {
name: string;
age: number;
}
const sem: Person = { name: 'semlinker', age: 30 };
type Sem= typeof sem; // -> Person
function toArray(x: number): Array<number> {
return [x];
}
type Func = typeof toArray; // -> (x: number) => number[]
2.keyof
keyof 操作符可以用来一个对象中的所有 key 值:
interface Person {
name: string;
age: number;
}
type K1 = keyof Person; // "name" | "age"
type K2 = keyof Person[]; // "length" | "toString" | "pop" | "push" | "concat" | "join"
type K3 = keyof { [x: string]: Person }; // string | number
- in 用来遍历枚举类型:
type Keys = "a" | "b" | "c"
type Obj = {
[p in Keys]: any
} // -> { a: any, b: any, c: any }
- infer
在条件类型语句中,可以用 infer 声明一个类型变量并且对它进行使用。
type ReturnType<T> = T extends (
...args: any[]
) => infer R ? R : any;
以上代码中 infer R 就是声明一个变量来承载传入函数签名的返回值类型,简单说就是用它取到函数返回值的类型方便之后使用。
- extends
有时候我们定义的泛型不想过于灵活或者说想继承某些类等,可以通过 extends 关键字添加泛型约束。
interface ILengthwise {
length: number;
}
function loggingIdentity<T extends ILengthwise>(arg: T): T {
console.log(arg.length);
return arg;
}
现在这个泛型函数被定义了约束,因此它不再是适用于任意类型:
loggingIdentity(3); // Error, number doesn't have a .length property
这时我们需要传入符合约束类型的值,必须包含必须的属性:
loggingIdentity({length: 10, value: 3});
- Partial
Partial<T> 的作用就是将某个类型里的属性全部变为可选项 ?。
定义:
/**
* node_modules/typescript/lib/lib.es5.d.ts
* Make all properties in T optional
*/
type Partial<T> = {
[P in keyof T]?: T[P];
};
在以上代码中,首先通过 keyof T 拿到 T 的所有属性名,然后使用 in 进行遍历,将值赋给 P,最后通过 T[P] 取得相应的属性值。中间的 ? 号,用于将所有属性变为可选。
示例:
interface Todo {
title: string;
description: string;
}
function updateTodo(todo: Todo, fieldsToUpdate: Partial<Todo>) {
return { ...todo, ...fieldsToUpdate };
}
const todo1 = {
title: "organize desk",
description: "clear clutter",
};
const todo2 = updateTodo(todo1, {
description: "throw out trash",
});
在上面的 updateTodo 方法中,我们利用 Partial<T> 工具类型,定义 fieldsToUpdate 的类型为 Partial<Todo>,即:
{
title?: string | undefined;
description?: string | undefined;
}
编译上下文
主要是一些vite+vue3项目的配置,如下:
tsconfig.json 的作用
- 用于标识 TypeScript 项目的根路径;
- 用于配置 TypeScript 编译器;
- 用于指定编译的文件。
tsconfig.json 重要字段
- files - 设置要编译的文件的名称;
- include - 设置需要进行编译的文件,支持路径模式匹配;
- exclude - 设置无需进行编译的文件,支持路径模式匹配;
- compilerOptions - 设置与编译流程相关的选项。
compilerOptions 选项
compilerOptions 支持很多选项,常见的有 baseUrl、 target、baseUrl、 moduleResolution 和 lib 等。
compilerOptions 每个选项的详细说明如下:
{
"compilerOptions": {
/* 基本选项 */
"target": "es5", // 指定 ECMAScript 目标版本: 'ES3' (default), 'ES5', 'ES6'/'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'
"module": "commonjs", // 指定使用模块: 'commonjs', 'amd', 'system', 'umd' or 'es2015'
"lib": [], // 指定要包含在编译中的库文件
"allowJs": true, // 允许编译 javascript 文件
"checkJs": true, // 报告 javascript 文件中的错误
"jsx": "preserve", // 指定 jsx 代码的生成: 'preserve', 'react-native', or 'react'
"declaration": true, // 生成相应的 '.d.ts' 文件
"sourceMap": true, // 生成相应的 '.map' 文件
"outFile": "./", // 将输出文件合并为一个文件
"outDir": "./", // 指定输出目录
"rootDir": "./", // 用来控制输出目录结构 --outDir.
"removeComments": true, // 删除编译后的所有的注释
"noEmit": true, // 不生成输出文件
"importHelpers": true, // 从 tslib 导入辅助工具函数
"isolatedModules": true, // 将每个文件做为单独的模块 (与 'ts.transpileModule' 类似).
/* 严格的类型检查选项 */
"strict": true, // 启用所有严格类型检查选项
"noImplicitAny": true, // 在表达式和声明上有隐含的 any类型时报错
"strictNullChecks": true, // 启用严格的 null 检查
"noImplicitThis": true, // 当 this 表达式值为 any 类型的时候,生成一个错误
"alwaysStrict": true, // 以严格模式检查每个模块,并在每个文件里加入 'use strict'
/* 额外的检查 */
"noUnusedLocals": true, // 有未使用的变量时,抛出错误
"noUnusedParameters": true, // 有未使用的参数时,抛出错误
"noImplicitReturns": true, // 并不是所有函数里的代码都有返回值时,抛出错误
"noFallthroughCasesInSwitch": true, // 报告 switch 语句的 fallthrough 错误。(即,不允许 switch 的 case 语句贯穿)
/* 模块解析选项 */
"moduleResolution": "node", // 选择模块解析策略: 'node' (Node.js) or 'classic' (TypeScript pre-1.6)
"baseUrl": "./", // 用于解析非相对模块名称的基目录
"paths": {}, // 模块名到基于 baseUrl 的路径映射的列表
"rootDirs": [], // 根文件夹列表,其组合内容表示项目运行时的结构内容
"typeRoots": [], // 包含类型声明的文件列表
"types": [], // 需要包含的类型声明文件名列表
"allowSyntheticDefaultImports": true, // 允许从没有设置默认导出的模块中默认导入。
/* Source Map Options */
"sourceRoot": "./", // 指定调试器应该找到 TypeScript 文件而不是源文件的位置
"mapRoot": "./", // 指定调试器应该找到映射文件而不是生成文件的位置
"inlineSourceMap": true, // 生成单个 soucemaps 文件,而不是将 sourcemaps 生成不同的文件
"inlineSources": true, // 将代码与 sourcemaps 生成到一个文件中,要求同时设置了 --inlineSourceMap 或 --sourceMap 属性
/* 其他选项 */
"experimentalDecorators": true, // 启用装饰器
"emitDecoratorMetadata": true // 为装饰器提供元数据的支持
}
}