前端入门-TS学习 | 豆包MarsCode AI刷题

222 阅读10分钟

outline:

  • 整体介绍: Ts 背景、优缺点、社区活跃度等

  • TS常用类型基本概念

    • 基础类型、对象类型、接口、断言等
  • 进阶用法

    • 类、泛型及使用场景
  • 工程向

    • 代码检测、编译配置、tsconfig介绍
    • 工程中最佳实践、迁移工具

01 为什么学习Ts?

01_Ts VS Js

TS VS JS.png

01_Ts带来了什么?

  • 类型安全
  • 下一代JS特性
  • 完善的工具链

TS不仅是一门语言,更是生产力工具

01_TS推荐学习资源

02 TypeScript基础

02_TS基础 - 基础类型

  1. boolean , number , string
  2. 枚举类型 enum
  3. 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);
}
  1. 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);
}
  1. null , undefined
    • null 和 undefined 各自有自己的类型,分别是 null 和 undefined
  2. 数组类型 []
  3. 元组类型 tuple

02_TS基础 - 函数类型

  • 定义: TS定义函数类型时要定义输入参数类型和输出类型
    • 输入参数: 参数支持可选参数和默认参数
    • 输出参数: 输出可以自动推断,没有返回值时,默认为void类型
  • 函数重载: 名称相同但参数不同,可以通过重载支持多种类型
    1. 定义多个函数签名: 在函数实现之前,定义多个函数签名,每个签名指定不同的参数类型和返回类型。
    2. 实现函数体: 在最后一个函数签名中,实现函数的具体逻辑。这个实现部分通常会处理所有可能的参数类型。
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 提供了一些基础的泛型操作符,如 keyoftypeofinextends 等。

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 提供了一些常用的泛型工具类型,如 PartialReadonlyPickOmit 等。

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: '非常棒!'
});