outline:
-
整体介绍: Ts 背景、优缺点、社区活跃度等
-
TS常用类型基本概念
- 基础类型、对象类型、接口、断言等
-
进阶用法
- 类、泛型及使用场景
-
工程向
- 代码检测、编译配置、tsconfig介绍
- 工程中最佳实践、迁移工具
01 为什么学习Ts?
01_Ts VS Js
01_Ts带来了什么?
- 类型安全
- 下一代JS特性
- 完善的工具链
TS不仅是一门语言,更是生产力工具
01_TS推荐学习资源
02 TypeScript基础
02_TS基础 - 基础类型
- boolean , number , string
- 枚举类型 enum
- any , unknown , void
-
any类型允许你绕过类型检查,可以对变量进行任何操作,但这也意味着你失去了 TypeScript 的类型安全保障 -
unknown也允许你存储任何值,但在你确定它的具体类型之前,你不能对它进行任何操作。这强制你在使用变量之前进行类型检查,从而保持了类型安全 -
void类型表示没有任何类型。通常用于函数没有返回值的情况
-
// unknown示例
let value: unknown;
value = 42; // OK
value = "Hello, world!"; // OK
value = [1, 2, 3]; // OK
// 以下操作会报错,因为 TypeScript 不知道 value 的具体类型
// let value1: number = value; // Error: Type 'unknown' is not assignable to type 'number'.
// let value2: string = value; // Error: Type 'unknown' is not assignable to type 'string'.
// 你需要在使用之前进行类型检查
if (typeof value === "number") {
let value3: number = value; // OK
console.log(value3);
} else if (typeof value === "string") {
let value4: string = value; // OK
console.log(value4);
}
- never
never类型表示那些永不存在的值的类型。例如,一个总是抛出异常的函数或一个无限循环的函数
function test(x: string | number): boolean {
if(typeof x == 'string') {
return true;
} else if (typeof x == 'number') {
return false;
}
return throwError('参数格式不对');
}
function throError(message: string) never {
throw new Error(message);
}
- null , undefined
null和undefined各自有自己的类型,分别是null和undefined
- 数组类型 []
- 元组类型 tuple
02_TS基础 - 函数类型
- 定义: TS定义函数类型时要定义输入参数类型和输出类型
- 输入参数: 参数支持可选参数和默认参数
- 输出参数: 输出可以自动推断,没有返回值时,默认为void类型
- 函数重载: 名称相同但参数不同,可以通过重载支持多种类型
- 定义多个函数签名: 在函数实现之前,定义多个函数签名,每个签名指定不同的参数类型和返回类型。
- 实现函数体: 在最后一个函数签名中,实现函数的具体逻辑。这个实现部分通常会处理所有可能的参数类型。
function add(x: number[]): number;
function add(x: string[]): string;
function add(x: any[]) any {
if(typeof x[0] == 'string') {
return x.join();
}
if(typeof x[0] == 'number') {
return x.reduce((acc,cur) => acc + cur);
}
}
// 调用函数
const result1 = add([1, 2, 3]); // 返回 6
const result2 = add(["a", "b", "c"]); // 返回 "a,b,c"
02_TS基础 - 接口interface
什么是接口?
接口是一种抽象的定义,它描述了一个对象应该具有的属性和方法。接口本身并不包含实现,它只是定义了对象的“形状”。
如何定义接口?
在 TypeScript 中,你可以使用 interface 关键字来定义一个接口。接口的名称通常以大写字母开头,并且遵循驼峰命名法。
interface Person {
name: string;
age: number;
sayHello(): void;
}
在这个例子中,我们定义了一个名为 Person 的接口,它有三个成员:
-
name:一个字符串类型的属性。 -
age:一个数字类型的属性。 -
sayHello:一个没有返回值(void)的方法。
如何使用接口?
定义了接口之后,你可以在代码中使用它来确保对象符合接口的定义。
let person: Person = {
name: "Alice",
age: 30,
sayHello: function() {
console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`);
}
};
person.sayHello(); // 输出: Hello, my name is Alice and I am 30 years old.
在这个例子中,我们创建了一个 person 对象,并确保它符合 Person 接口的定义。如果 person 对象缺少任何一个接口中定义的属性或方法,TypeScript 编译器会报错。
可选属性和只读属性
接口还可以定义可选属性和只读属性。
- 可选属性:使用
?标记属性为可选的。
interface Person {
name: string;
age?: number; // age 是可选的
sayHello(): void;
}
let person: Person = {
name: "Bob",
sayHello: function() {
console.log(`Hello, my name is ${this.name}.`);
}
};
person.sayHello(); // 输出: Hello, my name is Bob.
- 只读属性:使用
readonly关键字标记属性为只读的。
interface Person {
readonly id: number;
name: string;
age?: number;
sayHello(): void;
}
let person: Person = {
id: 1,
name: "Charlie",
sayHello: function() {
console.log(`Hello, my name is ${this.name}.`);
}
};
// person.id = 2; // 错误: 无法分配到 'id' ,因为它是只读属性。
接口的扩展
接口可以继承其他接口,从而扩展其定义。
interface Employee extends Person {
employeeId: number;
department: string;
}
let employee: Employee = {
id: 2,
name: "David",
age: 25,
employeeId: 1001,
department: "Engineering",
sayHello: function() {
console.log(`Hello, my name is ${this.name} and I work in ${this.department}.`);
}
};
employee.sayHello(); // 输出: Hello, my name is David and I work in Engineering.
在这个例子中,Employee 接口继承了 Person 接口,并添加了 employeeId 和 department 属性。
02_TS基础-类
什么是类?
类是一种模板或蓝图,用于创建具有相同属性和方法的对象。类定义了对象的结构和行为,并提供了创建对象的机制。
如何定义类?
在 TypeScript 中,你可以使用 class 关键字来定义一个类。类通常包含以下几个部分:
-
构造函数(Constructor):用于初始化对象的属性。
-
属性(Properties):对象的状态或数据。
-
方法(Methods):对象的行为或操作。
class Person {
// 属性
name: string;
age: number;
// 构造函数
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
// 方法
sayHello(): void {
console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`);
}
}
在这个例子中,我们定义了一个名为 Person 的类,它有两个属性 name 和 age,一个构造函数用于初始化这些属性,以及一个 sayHello 方法用于输出个人信息。
如何使用类?
定义了类之后,你可以使用 new 关键字来创建类的实例(对象)。
let person = new Person("Alice", 30);
person.sayHello(); // 输出: Hello, my name is Alice and I am 30 years old.
在这个例子中,我们创建了一个 `Person` 类的实例 `person`,并通过构造函数传入了 `name` 和 `age` 的值。然后,我们调用了 `sayHello` 方法来输出个人信息。
类的继承
类可以通过继承来扩展其他类的功能。继承允许你创建一个新类,该类继承了另一个类的属性和方法,并可以添加新的属性和方法。
class Employee extends Person {
// 新属性
employeeId: number;
department: string;
// 构造函数
constructor(name: string, age: number, employeeId: number, department: string) {
super(name, age); // 调用父类的构造函数
this.employeeId = employeeId;
this.department = department;
}
// 新方法
sayHello(): void {
console.log(`Hello, my name is ${this.name}, I am ${this.age} years old, and I work in ${this.department}.`);
}
}
let employee = new Employee("Bob", 25, 1001, "Engineering");
employee.sayHello(); // 输出: Hello, my name is Bob, I am 25 years old, and I work in Engineering.
在这个例子中,我们定义了一个名为 Employee 的类,它继承了 Person 类。Employee 类添加了两个新属性 employeeId 和 department,并重写了 sayHello 方法。在构造函数中,我们使用 super 关键字调用父类 Person 的构造函数来初始化 name 和 age 属性。
访问修饰符
TypeScript 提供了三种访问修饰符来控制类成员的访问权限:
-
public:默认访问修饰符,表示成员可以被任何地方访问。 -
private:表示成员只能在类的内部访问。 -
protected:表示成员可以在类的内部和子类中访问。
class Person {
public name: string;
private age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
public sayHello(): void {
console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`);
}
}
let person = new Person("Alice", 30);
console.log(person.name); // 输出: Alice
// console.log(person.age); // 错误: 'age' 是私有的,只能在 Person 类中访问。
person.sayHello(); // 输出: Hello, my name is Alice and I am 30 years old.
在这个例子中,name 属性是 public 的,可以在类的外部访问;而 age 属性是 private 的,只能在类的内部访问。
可选属性和只读属性
类中的可选属性在以下情况下非常有必要:
-
处理不同配置的对象:允许对象在创建时灵活地选择是否提供某些属性。
-
处理部分初始化的对象:允许对象在创建后逐步初始化某些属性。
-
处理不同版本的 API 或数据源:允许对象灵活地处理不同版本中的属性差异。
抽象类
abstract关键字- 不能被实例化,只能被继承
- 作为基类, 抽象方法必须由子类实现
- 抽象类和接口可以结合使用,以实现更复杂的类型约束; interface约束类, 使用
implements关键字
03 TypeScript进阶
03_TS进阶 - 高级类型
1. 联合类型(Union Types)
联合类型允许你定义一个变量或参数可以是多种类型之一。使用 | 符号来定义联合类型。
let value: string | number;
value = "Hello"; // 合法
value = 42; // 合法
// value = true; // 错误: 类型 'boolean' 不能赋值给类型 'string | number'
function printValue(value: string | number) {
console.log(value);
}
printValue("Hello"); // 输出: Hello
printValue(42); // 输出: 42
在这个例子中,value 变量的类型是 string | number,因此它可以存储字符串或数字类型的值。printValue 函数接受一个 string | number 类型的参数,并输出该值。
2. 交叉类型(Intersection Types)
交叉类型允许你将多个类型合并为一个类型。使用 & 符号来定义交叉类型。
interface Person {
name: string;
age: number;
}
interface Employee {
employeeId: number;
department: string;
}
type EmployeePerson = Person & Employee;
let employee: EmployeePerson = {
name: "Alice",
age: 30,
employeeId: 1001,
department: "Engineering"
};
console.log(employee.name); // 输出: Alice
console.log(employee.employeeId); // 输出: 1001
在这个例子中,EmployeePerson 类型是 Person 和 Employee 类型的交叉类型,因此它包含了 Person 和 Employee 的所有属性。employee 变量的类型是 EmployeePerson,因此它必须包含 Person 和 Employee 的所有属性。
3. 类型断言(Type Assertions)
类型断言允许你告诉 TypeScript 编译器某个值的具体类型,从而绕过类型检查。使用 as 关键字或尖括号语法来定义类型断言。
let someValue: any = "this is a string";
let strLength: number = (someValue as string).length;
// 或者使用尖括号语法
// let strLength: number = (<string>someValue).length;
console.log(strLength); // 输出: 16
在这个例子中,someValue 变量的类型是 any,但我们知道它实际上是一个字符串。通过使用类型断言 as string,我们告诉 TypeScript 编译器将 someValue 视为字符串类型,从而可以调用 length 属性。
4. 类型别名(Type Alias)与接口(Interface)
类型别名和接口都可以用于定义对象的结构,但它们有一些区别:
-
类型别名:使用
type关键字定义,可以用于任何类型,包括基本类型、联合类型、交叉类型、元组等。 -
接口:使用
interface关键字定义,主要用于定义对象的结构,可以被扩展(通过extends关键字)。
// 使用类型别名定义对象结构
type PersonType = {
name: string;
age: number;
};
// 使用接口定义对象结构
interface PersonInterface {
name: string;
age: number;
}
// 类型别名可以定义联合类型
type StringOrNumber = string | number;
// 接口可以扩展
interface EmployeeInterface extends PersonInterface {
employeeId: number;
}
// 类型别名可以通过交叉类型实现类似效果
type EmployeeType = PersonType & {
employeeId: number;
};
在这个例子中,我们分别使用类型别名和接口定义了 Person 类型,并展示了类型别名定义联合类型和接口扩展的用法。
03_TS进阶 - 泛型
什么时候需要泛型?
当你需要编写一个可以处理多种类型的函数或类时,泛型非常有用。泛型允许你在定义函数、类或接口时,不指定具体的类型,而是在使用时再指定类型。
基本使用
function identity<T>(arg: T): T {
return arg;
}
let output1 = identity<string>("Hello");
let output2 = identity<number>(42);
console.log(output1); // 输出: Hello
console.log(output2); // 输出: 42
在这个例子中,identity 函数是一个泛型函数,它接受一个类型参数 T,并返回相同类型的值。通过在调用函数时指定类型参数,我们可以处理不同类型的值。
03_TS进阶 - 泛型工具类型
基础操作符
TypeScript 提供了一些基础的泛型操作符,如 keyof、typeof、in、extends 等。
interface Person {
name: string;
age: number;
}
type PersonKeys = keyof Person; // 'name' | 'age'
let key: PersonKeys = "name"; // 合法
// key = "address"; // 错误: 类型 '"address"' 不能赋值给类型 '"name" | "age"'
let person = { name: "Alice", age: 30 };
type PersonType = typeof person; // { name: string, age: number }
在这个例子中,keyof 操作符用于获取 Person 接口的所有键,typeof 操作符用于获取 person 对象的类型。
常用工具类型
TypeScript 提供了一些常用的泛型工具类型,如 Partial、Readonly、Pick、Omit 等。
interface Person {
name: string;
age: number;
}
// Partial 将所有属性变为可选
type PartialPerson = Partial<Person>;
let partialPerson: PartialPerson = { name: "Alice" }; // 合法
// Readonly 将所有属性变为只读
type ReadonlyPerson = Readonly<Person>;
let readonlyPerson: ReadonlyPerson = { name: "Alice", age: 30 };
// readonlyPerson.name = "Bob"; // 错误: 无法分配到 'name' ,因为它是只读属性。
// Pick 从类型中选择部分属性
type NameOnly = Pick<Person, "name">;
let nameOnly: NameOnly = { name: "Alice" }; // 合法
// Omit 从类型中排除部分属性
type WithoutAge = Omit<Person, "age">;
let withoutAge: WithoutAge = { name: "Alice" }; // 合法
04 TypeScrip实战
04_TS实战 - 声明文件
- declare:三方库需要类型声明文件
- .d.ts:声明文件定义
- @types:三方库TS类型包
- tsconfig.json:定义TS的配置
04_TS实战 - 泛型约束后端接口类型
import axios from 'axios';
interface API {
'/book/detail': {
id: number;
};
'/book/comment': {
id: number;
comment: string;
};
}
function request<T extends keyof API>(url: T, obj: API[T]) {
return axios.post(url, obj);
}
request('/book/comment', {
id: 1,
comment: '非常棒!'
});