第11期 - 路边插画

封面图来源于上海淡水路,周末闲逛到喜欢的英语博主开的小书店,发现这条路很有格调。

技术分享-ts相关

高级类型

泛型-参数化类型

在定义函数、类或接口时使用参数来表示类型,参数可以在使用时指定具体的类型。尖括号 <> 通常用于表示泛型, 类型变量表示参数化的类型,通常用大写字母(如 T, U, K 等)来表示

函数泛型:

function identity<T>(arg: T): T {
    return arg;
}
let result: number = identity<number>(42); // 在尖括号中明确指定泛型类型
let result2: string = identity("Hello");   // TypeScript 根据参数类型推断泛型类型

接口泛型:

interface ArrayType<T> {
    array: T[];
}
let arr: ArrayType<number> = { array: [1, 2, 3] };

类泛型:

class Pair<T, U> {
    constructor(public first: T, public second: U) {}
}
let pair: Pair<string, number> = new Pair("one", 1);

类型别名泛型:

type Pair<T, U> = { first: T, second: U };
let pair: Pair<string, number> = { first: "one", second: 1 };

映射类型

在映射类型里,新类型以相同的形式去转换旧类型里每个属性,基于一些已存在的类型,且按照一定的方式转换字段。(这就是 keyof和索引访问类型要做的事情)内部使用了 for .. in,类型变量 K,它会依次绑定到每个属性

type Keys = 'option1' | 'option2';
type Flags = { [K in Keys]: boolean };
// 映射为
type Flags = {
    option1: boolean;
    option2: boolean;
}

如:可以令每个属性成为 readonly类型或可选的

interface PersonPartial {
    name?: string;
    age?: number;
}
// 转换为
interface PersonReadonly {
    readonly name: string;
    readonly age: number;
}
  • T[K] 索引访问类型,表示类型 T 中属性 K 所对应的类型
type T = {
    name: string;
    age: number;
    email: string;
};
type NameType = T['name']; // 这里 NameType 类型将会是 string
  • typeof typeof 操作符可以用来获取一个变量或对象的类型。typeof操作的是一个变量或对象
interface Person {
  name: string;
  age: number;
}
const sem: Person = { name: "semlinker", age: 30 };
type Sem = typeof sem; // type Sem = Person

// 注意下面这种写法是错误的:
const Sem = typeof sem;
  • keyof T keyof 可以获取一个类型所有键值,返回一个联合类型
type Person = {
  name: string;
  age: number;
}
type PersonKey = keyof Person;  // PersonKey得到的类型为 'name' | 'age'

预定义映射类型

  • Partial 将某个类型里的属性全部变为可选项 首先通过 keyof T 拿到 T 的所有属性名,然后使用 in 进行遍历,将值赋给 P,最后通过 T[P] 取得相应的属性值。中间的 ? 号,用于将所有属性变为可选。
type Partial<T> = {
    [P in keyof T]?: T[P];
}

interface Person {
    name: string;
    age: number;
    email: string;
}
// 使用 Partial 将 Person 类型的所有属性变为可选属性
type PartialPerson = Partial<Person>;

// PartialPerson 类型将会是以下形式:
// {
//     name?: string | undefined;
//     age?: number | undefined;
//     email?: string | undefined;
// }
  • Pick 从一个复合类型中,取出几个想要的类型的组合
type Pick<T, K extends keyof T> = {
    [P in K]: T[P];
}

// 原始类型
interface TState {
	name: string;
	age: number;
	like: string[];
}
// 如果我只想要name和age怎么办,最粗暴的就是直接再定义一个
// 这样的弊端是什么?就是在Tstate发生改变的时候,TSingleState并不会跟着一起改变
interface TSingleState {
	name: string;
	age: number;
}
// 正确做法 使用 Pick 实现类型共享
interface TSingleState extends Pick<TState, "name" | "age"> {};
  • Omit<T, K> Omit 用于创建一个新的类型,该类型移除了指定类型K中的属性。
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;

interface Person {
    name: string;
    age: number;
    email: string;
}

type PersonWithoutEmail = Omit<Person, 'email'>;
// PersonWithoutEmail 类型将是 { name: string; age: number; }
  • Extract<T, U> Extract<T, U> 的作用是从 T 中提取出 U 。通俗来讲就是在T里面查找符合U类型要求的类型字段,注意 只取符合要求的字段作为类型输出,而非直接取属性类型
type Person = {
    name: string;
    age: number;
    email: string;
    isAdmin: boolean;
};
// StringFields 类型将是 'name' | 'email'
type StringFields = Extract<Person[keyof Person], string>;
  • Exclude<T, U> 创建一个新的类型,该类型移除了两个类型之间的共同部分
type Numbers = 'one' | 'two' | 'three' | 'four';
type SmallNumbers = 'one' | 'two';

type BigNumbers = Exclude<Numbers, SmallNumbers>;

// BigNumbers 类型将是 'three' | 'four'
  • ReturnType 用于获取函数 T 的返回类型
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;

type T0 = ReturnType<() => string>; // string
type T1 = ReturnType<(s: string) => void>; // void

function f1(s: string) {
  return { a: 1, b: s };
}
// 即使没有显式地指定函数的返回类型,编译器也会根据函数的实际返回值进行类型推断
type T14 = ReturnType<typeof f1>;  // { a: number, b: string }

infer:用于在类型表达式中声明一个占位符,让编译器推断出具体的类型

  • Readonly
type Readonly<T> = {
    readonly [P in keyof T]: T[P];
}
  • Record<K, T> 创建具有特定键类型和值类型的对象类型 K 表示对象的键的类型 T 表示对象的值的类型
type Record<K extends string, T> = {
    [P in K]: T;
}

索引类型

定义可以通过索引访问的对象的类型

// 数字索引类型
interface NumberIndexed {
    [index: number]: string;
}
let obj: NumberIndexed = {
    0: "a",
    1: "b",
    2: "c"
};
console.log(obj[0]); // Output: "a"
// 字符串索引类型
interface StringIndexed {
    [index: string]: number;
}
let obj: StringIndexed = {
    "a": 1,
    "b": 2,
    "c": 3
};
console.log(obj["b"]); // Output: 2

字符串索引签名

字符串索引签名是一种定义对象属性的方法,允许为对象定义一个特殊的属性,该属性的键是字符串类型,值可以是任意类型.字符串索引签名允许创建具有任意字符串键的对象

interface StringIndexed {
    [index: string]: any;
}
let obj: StringIndexed = {
    "name": "John",
    "age": 30,
    "city": "New York"
};
console.log(obj["name"]); // Output: "John"

类定义会创建两个东西:类的实例类型和一个构造函数。

class Greeter {
    greeting: string;
    constructor(message: string) {
        this.greeting = message;
    }
    greet() {
        return "Hello, " + this.greeting;
    }
}

let greeter = new Greeter("world");

继承

类从基类中继承了属性和方法. Dog是一个 派生类,它派生自 Animal 基类,通过 extends关键字。 派生类通常被称作 子类,基类通常被称作 超类。

class Animal {
    move(distanceInMeters: number = 0) {
        console.log(`Animal moved ${distanceInMeters}m.`);
    }
}
class Dog extends Animal {
    bark() {
        console.log('Woof! Woof!');
    }
}
const dog = new Dog();
dog.bark();
dog.move(10);
dog.bark();

特殊场景:如果派生类包含了一个构造函数,必须调用 super(),它会执行基类的构造函数。 而且在构造函数里访问 this的属性之前,一定要调用 super()。且子类里可以重写父类的方法

public(默认)

可以明确的将一个成员标记成 public

class Animal {
    public name: string;
    public constructor(theName: string) { this.name = theName; }
    public move(distanceInMeters: number) {
        console.log(`${this.name} moved ${distanceInMeters}m.`);
    }
}

private

当成员被标记成 private时, 不能在声明它的类的外部访问

class Animal {
    private name: string;
    constructor(theName: string) { this.name = theName; }
}

new Animal("Cat").name; // 错误: 'name' 是私有的.

protected

与 private修饰符很相似,但 protected 成员在派生类中仍然可以访问, 但是 由派生类的实例去访问是错误的,仅限于派生类内部使用

class Person {
    protected name: string;
    constructor(name: string) { this.name = name; }
}
class Employee extends Person {
    private department: string;

    constructor(name: string, department: string) {
        super(name)
        this.department = department;
    }
    public getElevatorPitch() {
        return `Hello, my name is ${this.name} and I work in ${this.department}.`;
    }
}
let howard = new Employee("Howard", "Sales");
console.log(howard.getElevatorPitch());
console.log(howard.name); // 错误

标记构造函数protected, 意味着这个类不能在包含它的类外被实例化,但是能被继承

class Person {
    protected name: string;
    protected constructor(theName: string) { this.name = theName; }
}
// Employee 能够继承 Person
class Employee extends Person {
    private department: string;
    constructor(name: string, department: string) {
        super(name);
        this.department = department;
    }
    public getElevatorPitch() {
        return `Hello, my name is ${this.name} and I work in ${this.department}.`;
    }
}
let howard = new Employee("Howard", "Sales");
let john = new Person("John"); // 错误: 'Person' 的构造函数是被保护的.

readonly

使用 readonly关键字将属性设置为只读的, 只读属性必须在声明时或构造函数里被初始化

class Octopus {
    readonly name: string;
    readonly numberOfLegs: number = 8;
    constructor (theName: string) {
        this.name = theName;
    }
}
let dad = new Octopus("Man with the 8 strong legs");
dad.name = "Man with the 3-piece suit"; // 错误! name 是只读的.

// 参数属性 与上面等效 把声明和赋值合并至一处
class Octopus {
    readonly numberOfLegs: number = 8;
    constructor(readonly name: string) {
    }
}

static 静态属性

仅当类被实例化的时候才会被初始化的属性,创建类的静态成员,这些属性存在于类本身上面而不是类的实例上。需要 类名.来访问静态属性

class Grid {
    static origin = {x: 0, y: 0};
    calculateDistanceFromOrigin(point: {x: number; y: number;}) {
        let xDist = (point.x - Grid.origin.x);
        let yDist = (point.y - Grid.origin.y);
        return Math.sqrt(xDist * xDist + yDist * yDist) / this.scale;
    }
    constructor (public scale: number) { }
}

let grid1 = new Grid(1.0);  // 1x scale
let grid2 = new Grid(5.0);  // 5x scale

console.log(grid1.calculateDistanceFromOrigin({x: 10, y: 10}));
console.log(grid2.calculateDistanceFromOrigin({x: 10, y: 10}));

抽象类中的抽象方法不包含具体实现并且必须在派生类中实现,定义方法签名但不包含方法体。抽象方法必须包含 abstract关键字并且可以包含访问修饰符

abstract class Department {
    constructor(public name: string) {
    }
    printName(): void {
        console.log('Department name: ' + this.name);
    }
    abstract printMeeting(): void; // 必须在派生类中实现
}
class AccountingDepartment extends Department {
    constructor() {
        super('Accounting and Auditing'); // 在派生类的构造函数中必须调用 super()
    }
    printMeeting(): void {
        console.log('The Accounting Department meets each Monday at 10am.');
    }
    generateReports(): void {
        console.log('Generating accounting reports...');
    }
}

let department: Department; // 允许创建一个对抽象类型的引用
department = new Department(); // 错误: 不能创建一个抽象类的实例
department = new AccountingDepartment(); // 允许对一个抽象子类进行实例化和赋值
department.printName();
department.printMeeting();
department.generateReports(); // 错误: 方法在声明的抽象类中不存在

构造函数

用来表示类的构造函数类型

class Greeter {
    static standardGreeting = "Hello, there";
    greeting: string;
    greet() {
        if (this.greeting) {
            return "Hello, " + this.greeting;
        }
        else {
            return Greeter.standardGreeting;
        }
    }
}
// let greeter: Greeter,意思是 Greeter类的实例的类型是 Greeter
let greeter1: Greeter;
// 实例化 Greeter类,并使用这个对象
greeter1 = new Greeter();
console.log(greeter1.greet());
// 使用 typeof Greeter,意思是取Greeter类的类型,而不是实例的类型, 类型包含了类的所有静态成员和构造函数。
let greeterMaker: typeof Greeter = Greeter;
greeterMaker.standardGreeting = "Hey there!";

let greeter2: Greeter = new greeterMaker();
console.log(greeter2.greet());

ts 场景应用

项目中引入包,需要安装使用 @types为其添加声明文件,如:

npm install @types/jquery --save-dev

允许全局使用, 通过配置 tsconfig.json 的 compilerOptions.types 选项

{
  "compilerOptions": {
    "types" : [
      "jquery"
    ]
  }
}

如果第三方库自带类型定义文件(通常以 .d.ts 结尾) 在安装该库时,类型定义文件也会一同安装。这样在项目中直接使用该库时,TypeScript 就能够自动获取到类型定义。而无需额外的操作

如果第三方库没有提供类型定义文件,则需要自己定义 以 lodash 为例,创建一个名为 lodash.d.ts 的文件

// types/lodash.d.ts
declare module 'lodash' {
    interface LoDashStatic {
        // 定义 LoDashStatic 接口
        chunk<T>(array: List<T>, size?: number): T[][];
        // 其他 lodash 方法的类型定义可以类似地添加
    }

    export const _: LoDashStatic;
}

绕过 TypeScript 对 jquery 库的类型检查

// global.d.ts
declare module 'jquery' {
    export = jQuery;
}
// 使用 declare module 'jquery' 来声明一个模块,并在其中定义了 jQuery 和 $ 全局变量, 从而忽略 ts 对 jquery 模块的类型进行检查
declare var jQuery: JQueryStatic;
declare var $: JQueryStatic;

基础

类型声明空间

class Foo {}
interface Bar {}
type Bas = {};

变量声明空间

全局模块(全局空间):在全局作用域中声明的变量、函数、类等,在整个 TypeScript 文件中可见的变量、函数、类等的声明区域

// global.d.ts
interface GlobalInterface {
  name: string;
  age: number;
}
type GlobalType = {
  id: number;
  value: string;
};

文件模块(模块空间):在模块中声明的变量、函数、类等,只在该模块内可见,需要使用 export 导出才能在其他模块中使用。

class Foo {}
export const someVar = Foo; // 模块空间

命名空间:通过 namespace 关键字声明的命名空间,可以将相关的变量、函数、类等组织在一起,成员默认是定义在全局命名空间下的,确保namespace内创建的变量不会泄漏至全局命名空间, 避免全局作用域的污染。(命名空间内的成员如果要在命名空间外部被访问到,必须使用 export 关键字进行导出。)

// userTypes.ts
namespace UserTypes {
  export interface User {
    id: number;
    name: string;
    age: number;
  }
  export interface CreateUserInput {
    name: string;
    age: number;
  }
  export interface UpdateUserInput {
    id: number;
    name?: string;
    age?: number;
  }
}

// userService.ts
import { UserTypes } from "./userTypes";
function createUser(userData: UserTypes.CreateUserInput): UserTypes.User {
  // 创建用户逻辑
}
function updateUser(userId: number, userData: UserTypes.UpdateUserInput): UserTypes.User {
  // 更新用户逻辑
}

declare

在代码中引入第三方库或者现有的 JavaScript 模块,并为其提供类型声明。使用 declare module 来创建一个模块的类型声明,并在其中描述模块的类型信息,以便 TypeScript 正确地进行类型检查和提示

// jQuery.d.ts
declare module "jquery" {
  // 导出一个 jQuery 对象的类型声明
  export function $(selector: string): JQuery;
  export function $(element: HTMLElement): JQuery;
  // 定义 jQuery 对象的接口
  interface JQuery {
    // 定义 jQuery 对象的方法
    css(property: string, value: any): JQuery;
    click(handler: () => void): JQuery;
    // 其他方法...
  }
}

// main.ts
import * as $ from "jquery";
// 使用 jQuery 对象
$("button").click(() => {
  console.log("Button clicked");
});

基础类型

联合类型

从两个对象中创建一个新对象,新对象拥有着两个对象所有的功能

string | number

交叉类型

interface Dog {
  name: string;
  run(): void;
}
interface Bird {
  name: string;
  fly(): void;
}
// 交叉类型 Dog & Bird 表示同时具有 Dog 和 Bird 类型的特性
type DogAndBird = Dog & Bird;

元组类型

let nameNumber: [string, number];
// Ok
nameNumber = ['Jenny', 221345];

Void

void类型像是与any类型相反,它表示没有任何类型。 当一个函数没有返回值时,其返回值类型是 void

function warnUser(): void {
    console.log("This is my warning message");
}

Null 和 Undefined

默认情况下null和undefined是所有类型的子类型。

unknown

unknown 是 TypeScript 中的一个顶级类型,它表示“未知”类型。与 any 类型不同,unknown 类型提供了更严格的类型检查。当一个变量被声明为 unknown 类型时,它可以接受任何类型的值

let userInput: unknown;
let userName: string;

userInput = 5;
userInput = "John";

// 在使用前需要进行类型检查或类型断言
if (typeof userInput === "string") {
    userName = userInput; // 正确,因为现在 userInput 被类型断言为 string
}

枚举

默认情况下,从0开始为元素编号, 也可以手动的指定成员的数值, 或者全部都采用手动赋值

enum Color {Red, Green, Blue} // 默认从0开始
enum Color {Red = 1, Green, Blue} // 默认从1开始
enum Color {Red = 1, Green = 2, Blue = 4} // 指定值
let c: Color = Color.Green;

Never

永不存在值的类型, never类型是那些总是会抛出异常或根本就不会有返回值的函数表达式或箭头函数表达式的返回值类型 (never类型是任何类型的子类型,也可以赋值给任何类型;然而,没有类型是never的子类型或可以赋值给never类型(除了never本身之外)。 即使any也不可以赋值给never。)

// 返回never的函数必须存在无法达到的终点
function error(message: string): never {
    throw new Error(message);
}
// 推断的返回值类型为never
function fail() {
    return error("Something failed");
}
// 返回never的函数必须存在无法达到的终点
function infiniteLoop(): never {
    while (true) {
    }
}

类型别名

type StrOrNum = string | number;

类型约束

使用泛型时,对泛型参数进行限制,两种方式进行类型约束 extends 关键字

function printName<T extends { name: string }>(obj: T) {
    console.log(obj.name);
}
printName({ name: 'John' }); // 合法
printName({ age: 30 });      // 编译错误,参数不满足约束条件

交叉类型或联合类型(扩展泛型参数的类型范围)

function merge<T extends object, U extends object>(obj1: T, obj2: U): T & U {
    return { ...obj1, ...obj2 };
}
const mergedObj = merge({ name: 'John' }, { age: 30 }); // 合法

类型断言

通过类型断言这种方式可以告诉编译器,不进行特殊的数据检查和解构。 它没有运行时的影响,只是在编译阶段起作用。

let someValue: any = "this is a string";
let strLength: number = (<string>someValue).length; // 等价
let strLength: number = (someValue as string).length; // 等价

一些常见问题

type和interface有什么区别 type 关键字用于创建类型别名,允许为现有的类型创建一个新的名字。也可以用于定义组合类型,包括联合类型和交叉类型

type StringOrNumber = string | number;
type Point = { x: number; y: number };
type Shape = Circle | Square;

interface 用于描述对象的形状,包括属性和方法。可以被继承或实现,从而可以扩展现有的接口。还可用于进行类型检查和类型推断

interface Person {
    name: string;
    age: number;
    sayHello(): void;
}
interface Dog extends Person {
    bark(): void;
}

type 用于创建类型别名和组合类型,而 interface 则用于描述对象的形状和行为,并具有继承和实现的特性.