欢迎光临
我们一直在努力

TypeScript学习笔记(Part—2)

一、对象

1.1 对象定义

// 匿名方式定义对象。
let a: { name: string; age: number } = { name: "xiaoming", age: 18 };

// 接口命名方式定义对象。
interface B {
name: string;
age: number;
}
let b: B = { name: "xiaoming", age: 18 };

// 类型别名方式定义对象。
type C = { name: string; age: number };
let c: C = { name: "xiaoming", age: 18 };

1.2 可选属性(?)

// 在属性名与冒号之间添加一个问号。
interface ABC {
x: number;
y?: number;
z?: number;
}

let a: ABC = { x: 1 };

/*
若使用了可选属性,在函数中作为参数时,
要么在函数中用if等判断哪些属性没传值,
要么使用解构在参数中设置默认值。
*/

1.3 只读属性(readonly)

1.3.1 基础语法

// 在属性名前添加关键词“readonly”。
interface ABC {
id: number;

readonly name: string;
readonly address: {
a1: string;
a2: string;
};
}

function test(abc: ABC) {
console.log(abc.id, abc.name, abc.address.a1, abc.address.a2);

abc.id = 1;
abc.name = "abc"; // 无法为“name”赋值,因为它是只读属性。
abc.address.a1 = "abc"; // 正确。
abc.address.a2 = "abc"; // 正确。
abc.address = { // 无法为“address”赋值,因为它是只读属性。
a1: "abc",
a2: "abc",
};
}

interface Person {
name: string;
age: number;
}
interface ReadonlyPerson {
readonly name: string;
readonly age: number;
}

let p1: Person = { name: "Jim", age: 20 };
// ReadonlyPerson兼容Person。
let p2: ReadonlyPerson = p1;

p2.name = "Tom"; // 无法为“name”赋值,因为它是只读属性。
p2.age = 21; // 无法为“age”赋值,因为它是只读属性。

p1.name = "Jimmy";
p1.age = 22;

// p1更改后,p2也会改变。
console.log(p1, p2);
// { name: 'Jimmy', age: 22 } { name: 'Jimmy', age: 22 }。

1.3.2 详细介绍

错误理解:readonly只能用于数组和元组字面量类型(许多文档中关于readonly的介绍)。 正确理解:readonly既可以用于类型定义中的属性(如interface/class),也可以用于类型注解中的数组/元组(如readonly string[]) —— 这是两种不同但相关的用法。


readonly的两种主要用法:

  • 在接口/类中修饰属性。

    interface ABC {
    id: number;
    readonly name: string; // 合法:name属性只读。
    readonly address: { // 合法:address属性只读。
    a1: string;
    a2: string;
    };
    }

    • 作用:表示该属性在对象创建后不能被重新赋值。

    • 适用范围:interface、type、class中的属性。

    • 这是TypeScript从1.x就支持的特性。

    示例:

    const obj: ABC = {
    id: 1,
    name: "Alice",
    address: { a1: "Beijing", a2: "China" }
    };

    obj.name = "Bob"; // 错误!Cannot assign to 'name' because it is a read-only property.
    obj.address = {}; // 错误!address是只读属性

    obj.address.a1 = "Shanghai"; // 允许!因为address对象本身不是readonly。
    // 注意:`readonly address`表示`address`引用不可变,但`address`内部的对象属性默认仍可修改(除非内部也加`readonly`)。

  • 在类型注解中修饰数组/元组(“字面量”用法)。

    let arr: readonly number[] = [1, 2, 3]; // 只读数组。
    let tuple: readonly [string, number] = ["a", 1]; // 只读元组。

    • 作用:禁止调用push、splice等mutating方法,也禁止通过索引赋值(如arr[0] = 10)。

    • 这是TypeScript 3.4引入的readonly类型修饰符,用于简化ReadonlyArray<T>的写法。


  • 为什么很多文档会有“只能用于数组/元组”的介绍?

    可能是因为TypeScript官方文档在介绍readonly类型修饰符(readonly type modifier)时,重点强调了它在数组/元组上的新用法:

    • “TypeScript 3.4 introduced a new syntax for ReadonlyArray<T>: readonly T[].”

    但这并不意味着旧的readonly属性修饰符被废弃或限制!

    实际上,两者共存且用途不同:

    用法语法作用范围引入版本
    属性只读 readonly prop: T。 interface / class / type中的属性。 TS 1.0+
    数组/元组只读 readonly T[]或readonly [T, U]。 类型注解(变量、参数、返回值等)。 TS 3.4+

    readonly属性 vs Readonly<T>:(你还可以用工具类型Readonly<T>让整个对象只读)。

    interface User {
    name: string;
    age: number;
    }

    type ROUser = Readonly<User>;
    // 等价于:
    // {
    // readonly name: string;
    // readonly age: number;
    // }


    总结:

    interface ABC {
    id: number;
    readonly name: string; // 正确:属性只读(TS 1.0+支持)。
    readonly address: { }; // 正确。
    }

    • 这不是“仅允许数组/元组”的例外,而是readonly的原始设计用途之一。

    • TypeScript中的readonly有两个上下文:

    • 成员修饰符(用于interface/class属性)。

    • 类型前缀修饰符(用于readonly T[])。

    • 两者都是官方支持的标准特性,没有冲突,也不互相排斥。

    1.4 索引签名

    // 使用中括号将属性名括起来。
    // 当我们不关心属性名,只关心其属性名称数据类型、属性值数据类型时,我们可以使用索引签名。
    interface TestArray {
    [index: number]: string;
    }

    const myArray1: TestArray = ["a", "b", "c"];

    const myArray2: TestArray = [1, 2, 3];
    // 报错:不能将类型“number”分配给类型“string”。

    interface TestString {
    [index: string]: number;
    }

    const myObject1: TestString = { a: 1, b: 2, c: 3 };

    const myObject2: TestString = { 1: "a", 2: "b", 3: "c" };
    // 报错:不能将类型“string”分配给类型“number”。

    interface TestNumberAndString1 {
    name1: string;
    age1: number;
    [index: number]: number;
    name2: string;
    age2: number;
    }

    const myObject3: TestNumberAndString1 = {
    name1: "a",
    age1: 1,
    name2: "b",
    age2: 2,
    0: 1,
    1: 2,
    2: 3,
    8: 4,
    "9": 5, // 符合“[index: number]: number;”要求,会隐式类型转换。
    a: 6, // 报错:不能将类型“string”分配给类型“number”。
    };

    interface TestNumberAndString2 {
    name1: string; // 报错:类型“string”的属性“name1”不能赋给“string”索引类型“number”。
    age1: number;
    [index: number | string]: number;
    name2: string; // 报错:类型“string”的属性“name2”不能赋给“string”索引类型“number”。
    age2: number;
    }
    /*
    “[index: number | string]: number;”
    中的
    “[index: number | string]”
    包含了name1、name2、age1、age2,
    但name1、name2的属性值(string)不符合其要求(number)。
    */

    interface TestReadonly {
    readonly [index: number]: string; // 只读索引签名。
    }

    1.5 扩展类型(继承)

    // 使用extends实现继承。
    interface A {
    name: string;
    }

    interface B {
    age: number;
    }

    interface Dog extends A, B {
    breed: string;
    }

    1.6 交叉类型(&)

    // 使用“&”符号连接多个接口。
    interface Circle {
    radius: number;
    }
    interface Colorful {
    color: string;
    }

    type CircleAndColorful = Circle & Colorful;

    const circleAndColorful: CircleAndColorful = {
    radius: 1,
    color: "red",
    };

    function abc(cc: Circle & Colorful) {
    console.log(cc.radius);
    console.log(cc.color);
    }

    二、泛型

    2.1 泛型对象类型

    interface Box<Type> {
    content: Type;
    }
    let box: Box<string> = {
    content: "hello world",
    };

    type Box<Type> = {
    content: Type;
    };

    // 类型别名在泛型中的另一种用法(编写其它类型的通用的辅助类型)。

    type OrNull<Type> = Type | null; // 该类型定义的变量可为"Type"数据类型,也可为"null"。
    type OneOrMany<Type> = Type | Type[]; // 该类型定义的变量可为"Type"数据类型,也可为"Type"数据类型数组。
    type OneOrManyOrNull<Type> = OrNull<OneOrMany<Type>>; // "OrNull<OneOrMany<Type>>" => "OrNull<Type | Type[]>" => "Type | Type[] | null"。

    /*
    type OrNull<Type> = Type | null;
    这是一种TypeScript中的泛型类型别名(Generic Type Alias)的用法。
    这个类型别名的作用是:给任意类型"Type"添加"null"的可能性,从而表示“这个值可能是"Type"类型,也可能是"null"”。
    使用示例:
    type OrNull<T> = T | null;

    let name: OrNull<string> = "Alice"; // OK。
    name = null; // OK。

    let age: OrNull<number> = 25; // OK。
    age = null; // OK。

    let user: OrNull<{ id: number }> = { id: 1 }; // OK。
    user = null; // OK。
    这种模式在以下场景中非常有用:
    1、表示API返回的数据可能为空。
    2、处理可选的中间状态(比如加载中尚未获取到数据)。
    3、与后端或数据库交互时,字段可能为"null"。
    类似内置工具类型:
    TypeScript内置了类似的工具类型,比如:
    1、NonNullable<T>:从T中移除"null"和"undefined"。
    2、虽然没有内置 OrNull<T>,但你可以自己定义,如上所示。
    总结:
    type OrNull<Type> = Type | null;
    是一个自定义的泛型类型别名,用于让任意类型"Type"可以为"null",提高代码的可读性和类型安全性。
    */

    2.2 泛型类型

    function identity<Type>(arg: Type): Type {
    return arg;
    }

    // ………………………………………………………………………………….

    // 函数类型表达式声明(函数类型语法)。
    let myIdentity1 = identity;
    let myIdentity2: <Type>(arg: Type) => Type = identity;
    // myIdentity1和myIdentity2是等价的。
    // 前者使用的是TS自动类型推断,后者使用的是显示声明变量类型(通过函数类型表达式,特别的是该函数类型表达式用了泛型)。

    // ………………………………………………………………………………….

    // 调用签名声明(对象字面量语法)。
    let myIdentity3: { <Type>(arg: Type): Type } = identity;

    interface GenericIdentityFn1 {
    <Type>(arg: Type): Type;
    }
    let myIdentity4: GenericIdentityFn1 = identity;
    console.log(myIdentity4(1)); // <1>(arg: 1) => 1。
    console.log(myIdentity4<number>(1)); // <number>(arg: number) => number。

    interface GenericIdentityFn2<Type> {
    (arg: Type): Type;
    }
    let myIdentity5: GenericIdentityFn2<number> = identity;
    console.log(myIdentity5(1)); // (arg: number) => number。

    // 注意:当类型字面量只包含调用签名时,应该使用函数类型语法而不是对象字面量语法来定义函数类型。

    2.3 泛型类

    class GenericNumber<NumType> {
    constructor(zero: NumType, add: (x: NumType, y: NumType) => NumType) {
    this.zeroValue = zero;
    this.add = add;
    }

    zeroValue: NumType;
    add: (x: NumType, y: NumType) => NumType;
    }

    let myGenericNumber = new GenericNumber<number>(0, (x, y) => x + y);
    console.log(myGenericNumber.zeroValue, myGenericNumber.add(1, 5)); // 0 6。

    myGenericNumber.zeroValue = 8;
    myGenericNumber.add = function (x, y) {
    return x * y;
    };
    console.log(myGenericNumber.zeroValue, myGenericNumber.add(2, 5)); // 8 10。

    2.4 泛型约束

    2.4.1 基本语法

    // 使用extends。

    interface Lengthwise {
    length: number;
    }

    // 限制Type类型必须包含length属性。
    function loggingIdentity<Type extends Lengthwise>(arg: Type): Type {
    console.log(arg.length); // 3。
    return arg;
    }

    console.log(loggingIdentity([1, 2, 3])); // [ 1, 2, 3 ]。

    2.4.2 使用类型参数(keyof)

    // 使用keyof关键词。

    function getProperty<Type, Key extends keyof Type>(obj: Type, key: Key): Type[Key] {
    return obj[key];
    }

    let x = { a: 1, b: 2, c: 3, d: 4 };
    console.log(getProperty(x, "a")); // 1。
    let y = [1, 2, 3, 4, 5, 6];
    console.log(getProperty(y, 2)); // 3。


    在TypeScript中,keyof是一个类型操作符(type operator),用于获取某个类型的所有公共属性名(键)组成的联合类型(union type)。

    基本语法:

    type Keys = keyof T;

    • T必须是一个对象类型(如interface、type、class等)。

    • keyof T的结果是:所有属性名的字符串字面量联合类型。


    举例说明:

  • 基础用法。

    interface User {
    id: number;
    name: string;
    email: string;
    }

    type UserKeys = keyof User;
    // 等价于:
    // type UserKeys = "id" | "name" | "email";

    // ……………………………………………………………………………

    interface Arrayish1 {
    [n: number]: string;
    }
    interface Arrayish2 {
    [n: string]: string;
    // 注意:当索引签名为string类型时,实际上它是"string | number"的联合类型。详情如下解释。
    }

    type A = keyof Arrayish1; // 等价于:type UserKeys = number;
    let a: A = 1; // 正确。
    let b: A = "1"; // 报错:不能将类型“string”分配给类型“number”。

    type B = keyof Arrayish2; // 等价于:type UserKeys = string | number;
    let c: B = 1; // 正确。
    let d: B = "1"; // 正确。

    /*
    当对象的索引签名定义为"[n: string]: string"时,用number类型的键(比如obj[0])去访问,
    TypeScript却允许,并且把number键也视为合法 —— 甚至在某些上下文中,键的类型会被推断为"string | number"。

    这背后的原因是:
    JavaScript的对象属性名本质上都是字符串(或Symbol),
    而TypeScript为了与JavaScript的运行时行为保持一致,对数字键做了特殊处理。

    核心原因:
    JavaScript中,obj[0]等价于obj["0"],也就是说,所有通过数字访问的对象属性,最终都会被转成字符串。
    因此,TypeScript设计时决定:
    如果一个对象有string索引签名([key: string]: T),那么它也隐式支持number作为键,因为number会被自动转为string。

    索引签名为string,但可以用number访问。
    如果你只定义了number索引签名,则不能用任意string访问。
    即:string索引签名 => 兼容number键;number索引签名 ≠> 兼容任意string键。

    为什么有时看到键的类型是"string | number"?
    这通常出现在泛型约束或工具类型推导中。(eg:使用keyof获取索引签名对象的键类型)
    为什么keyof Dict是"string | number"?
    因为TypeScript认为:
    你可以用任意string键访问(如dict["name"])。
    也可以用任意number键访问(如dict[0] → 实际是dict["0"])。
    所以keyof必须包含两者,以保证类型安全。
    但这不意味着对象真的有number类型的属性,而是TypeScript在类型系统层面放宽了键的类型,以匹配JavaScript的运行时行为。

    为什么索引签名为string,可以用number访问。但如果只定义了number索引签名,却不能用任意string访问?
    "[key: number]: T"不会自动接受任意string键,因为并非所有字符串都能转换为有效数字。
    而"[key: string]: T"接受number键,是因为所有数字都可以无损转为字符串(如0 → "0")。
    这是TypeScript单向兼容的设计:只在安全、无歧义的方向上放宽类型限制。
    */

    此时,UserKeys只能是"id"、"name"或"email"中的一个:

    const key1: UserKeys = "name"; // 正确。
    const key2: UserKeys = "age"; // 错误!"age"不在User的属性中。

  • 与泛型结合:安全地访问对象属性(这是keyof最经典的用途)。

    function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
    return obj[key];
    }

    const user = { id: 1, name: "Alice", email: "a@example.com" };

    const name = getProperty(user, "name"); // 类型:string。
    const id = getProperty(user, "id"); // 类型:number。
    getProperty(user, "phone"); // 编译错误!

    • K extends keyof T:确保传入的key是T的合法属性。

    • 返回类型T[K]:TypeScript能自动推导出对应属性的类型(索引访问类型)。

  • 与typeof联用:获取具体对象的键。

    const config = {
    theme: "dark",
    lang: "zh",
    version: 2,
    };

    type ConfigKeys = keyof typeof config;
    // 等价于:"theme" | "lang" | "version"。

    注意:typeof config在类型上下文中表示“config的类型”,然后keyof提取其键。

  • 在映射类型(Mapped Types)中使用。

    keyof是实现工具类型(如Partial、Readonly)的基础。

    // 自定义只读类型。
    type MyReadonly<T> = {
    readonly [K in keyof T]: T[K];
    };

    interface Todo {
    title: string;
    done: boolean;
    }

    type ReadonlyTodo = MyReadonly<Todo>;
    // 相当于:
    // {
    // readonly title: string;
    // readonly done: boolean;
    // }

    这里[K in keyof T]表示:遍历T的每一个键K,并为每个键生成一个属性。


  • 注意事项:

  • 只包含 public 属性。

    keyof不会包含private或protected成员(因为它们在类型系统中不可见)。

  • 对基本类型的行为。

    对string、number等使用keyof会得到其原型方法名(一般不推荐这样用)。

    type S = keyof string; // 很多方法名,如"charAt", "slice", …。

  • 空对象类型。

    type Empty = keyof {}; // never。


  • 实际应用场景:

    场景说明
    通用属性访问函数 如getProperty,避免硬编码属性名。
    表单校验/动态字段 限制用户只能操作已知字段。
    API参数规范化 确保传入的字段名合法。
    工具类型实现 Partial<T>, Pick<T, K>, Omit<T, K>等都依赖keyof。

    总结:

    • keyof T → 获取类型T的所有属性名组成的联合类型。

    • 常与泛型、extends、T[K](索引访问类型)配合使用。

    • 是TypeScript类型安全和元编程能力的核心之一。

    • 编译后消失,零运行时开销。

    2.5 使用类类型

    // 工厂函数,专门创建类的(使用了构造签名)。
    function create<Type>(c: { new (): Type }): Type {
    return new c();
    }

    /*
    "c: { new (): Type }"中只包含构造签名。
    注意:当类型字面量只包含构造签名时,应该使用函数类型语法而不是对象字面量语法来定义函数类型。
    */

    eg:

    class Animal {
    numLegs: number;
    constructor(numLegs: number) {
    this.numLegs = numLegs;
    }
    }

    class Bird extends Animal {
    keeper: Animal = new Animal(2);
    }

    class House {
    numBedrooms: number;
    constructor(numBedrooms: number) {
    this.numBedrooms = numBedrooms;
    }
    }

    // "new (v: number): T"要与类的构造函数参数匹配。
    function create<T extends Animal>(c: { new (v: number): T }, val: number): T {
    return new c(val);
    }

    console.log(create(Bird, 6).keeper.numLegs); // 输出2。因为Bird类中"keeper: Animal = new Animal(2);"。

    2.6 typeof类型操作符

    2.6.1 基础语法

    function f() {
    return {
    a: 1,
    b: 2,
    };
    }
    type F = ReturnType<typeof f>;
    /*
    等价于:
    type F = {
    a: number;
    b: number;
    }
    */

    /*
    ReturnType<typeof f>是TS内置数据类型。其中f为函数(无返回值时,返回void)。
    作用:获取函数类型的返回类型。
    源码:type ReturnType<T extends (…args: any) => any> = T extends (…args: any) => infer R ? R : any;
    有关"infer"关键词,详情请看2.8节。
    */

    // ……………………………………………………………………………………….

    let d = {
    x: 1,
    y: 2,
    };

    type D = typeof d;
    /*
    等价于:
    type D = {
    x: number;
    y: number;
    }
    */

    // ……………………………………………………………………………………….

    function abc() {}

    type F = typeof abc;
    // 等价于:type F = () => void。

    // ……………………………………………………………………………………….

    type A = 'a';
    // 等价于:type A = 'a'。即变量类型为A时,只能为该变量赋值'a'。

    // 区别于JS的typeof,TS中的typeof并不会只返回function、object。

    2.6.2 索引访问类型

    interface Abc1 {
    name: string;
    }
    type A = Abc1["name"];
    // 等价于:type A = string。

    type Abc2 = {
    name: string;
    };
    type B = Abc2["name"];
    // 等价于:type B = string。

    // ……………………………………..

    type Abc3 = {
    name: string;
    age: number;
    };
    type C = Abc3["name" | "age"];
    // 等价于:type C = string | number。

    // ……………………………………..

    const myArray = [
    { name: "Alice", age: 18 },
    { name: "Bob", isAdult: true },
    { name: "Eve", childName: ["Alice", "Bob"] },
    ];

    type D = (typeof myArray)[number];
    // 也可写成"typeof myArray[number];"。下面的也一样,可以把小括号去掉。
    /*
    等价于:
    type D = {
    name: string;
    age: number;
    isAdult?: never;
    childName?: never;
    } | {
    name: string;
    isAdult: boolean;
    age?: never;
    childName?: never;
    } | {
    name: string;
    childName: string[];
    age?: never;
    isAdult?: never;
    }
    */

    type E = typeof myArray;
    /*
    等价于:
    type E = ({
    name: string;
    age: number;
    isAdult?: never;
    childName?: never;
    } | {
    name: string;
    isAdult: boolean;
    age?: never;
    childName?: never;
    } | {
    name: string;
    childName: string[];
    age?: never;
    isAdult?: never;
    })[]
    */

    type F = (typeof myArray)[number]["age"];
    /*
    等价于:
    type F = number | undefined。
    */

    type G = (typeof myArray)[2]["isAdult"];
    /*
    等价于:
    type G = boolean | undefined。
    */

    type H = (typeof myArray)[2]["name"];
    /*
    等价于:
    type H = string。
    因为数组中的所有对象都有该属性且都为string类型。
    */

    表达式含义
    T[string] 获取对象所有string键对应的值类型(常用于索引签名)。
    T[number] 获取数组/元组所有元素类型的联合。
    T[0] 获取元组第一个元素的类型。
    T[keyof T] 获取对象所有属性值的联合类型。

    2.7 条件类型

    2.7.1 基础语法

    // 使用三元表达式。
    interface Animal {
    live(): void;
    }
    interface Dog extends Animal {
    woof(): void;
    }

    type Example1 = Dog extends Animal ? number : string;
    // 等价于:type Example1 = number。
    type Example2 = RegExp extends Animal ? number : string;
    // 等价于:type Example2 = string。

    eg:(可取代部分函数重载实例,简化函数)

    interface Idlabel {
    id: number;
    }
    interface Namelabel {
    name: string;
    }

    // 函数重载,很麻烦,不宜阅读,且函数参数更多时,重载签名也会更多。
    function createLabel1(id: number): Idlabel;
    function createLabel1(name: string): Namelabel;
    function createLabel1(nameOrId: string | number): Idlabel | Namelabel {
    throw "unimplemented";
    }

    // 条件类型直接搞定。
    type NameOrId<T extends number | string> = T extends number ? Idlabel : Namelabel;

    function createLabel2<T extends number | string>(idOrName: T): NameOrId<T> {
    throw "unimplemented";
    }

    2.7.2 条件类型约束

    // 条件类型 + 泛型约束。

    type MessageOf<T> = T extends { message: unknown } ? T["message"] : never;
    /*
    也可写成:type MessageOf<T> = T extends { message: infer P } ? P : never;
    有关"infer"关键词,详情请看2.8节。
    */

    interface Email {
    message: string;
    }
    interface Dog {
    bark(): void;
    }

    type EmailMessageContents = MessageOf<Email>;
    const email: EmailMessageContents = "hello";

    type DogMessageContents = MessageOf<Dog>;
    const dog: DogMessageContents = "woof";
    // 报错:不能将类型“"woof"”分配给类型“never”。

    2.8 infer类型操作符

    在TypeScript中,infer是一个用于在条件类型(Conditional Types)中“推断”并捕获类型变量的关键字。它的核心作用是:从一个复杂类型中“提取”出你关心的部分,并将其绑定到一个类型参数上,供后续使用。

    简单说:infer就像类型层面的“模式匹配 + 解构赋值”。


    基本语法:

    type MyType<T> = T extends (infer U) ? U : never;

    • infer U出现在条件类型的extends子句的右侧。

    • 当T满足某种结构时,TypeScript会自动推断出U的具体类型。

    • 推断出的U可以在true分支中使用。


    核心用途:从类型结构中“提取”子类型。

  • 提取函数的返回类型(内置ReturnType的实现原理)。

    type ReturnType<T> = T extends (args: any[]) => infer R ? R : never;

    // 使用。
    type F = () => string;
    type R = ReturnType<F>; // string。

    infer R捕获了函数返回值的类型string。

  • 提取函数的参数类型(内置Parameters的实现)。

    type Parameters<T> = T extends (args: infer P) => any ? P : never;

    // 使用。
    type F = (a: number, b: string) => void;
    type P = Parameters<F>; // [number, string]。

    infer P捕获了参数列表的元组类型。

  • 提取Promise的resolve类型(内置Awaited的简化版)。

    type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;

    // 使用。
    type P = Promise<string>;
    type U = UnwrapPromise<P>; // string。
    type V = UnwrapPromise<number>; // number(不满足条件,返回原类型)。

    infer U从Promise<string>中提取出string。

  • 提取数组元素类型。

    type ElementType<T> = T extends (infer E)[] ? E : T;

    // 使用。
    type Arr = string[];
    type E = ElementType<Arr>; // type E = string。
    type F = ElementType<readonly string[]>; // type F = readonly string[]。

    注意:更健壮的写法应处理readonly数组:

    type ElementType<T> = T extends readonly (infer E)[] ? E : T;

    // 使用。
    type Arr = string[];
    type E = ElementType<Arr>; // type E = string。
    type F = ElementType<readonly string[]>; // type F = string。

    /*
    为什么此处泛型限制为只读数组后,传递普通数组时,依旧可判定为真?
    反而上述泛型限制为普通数组后,传递只读数组时,却判定为假?

    该问题触及了TypeScript类型系统中一个重要的兼容性设计原则。我们来一步步解释为什么:
    即使ElementType<T>中使用了"readonly (infer E)[]",普通的(非readonly)数组也能被匹配并正确推断出元素类型。
    核心原因:TypeScript中"T[]"是"readonly T[]"的子类型。
    也就是说:
    可变数组(mutable array)可以赋值给只读数组(readonly array)变量,因此在类型匹配时,"number[]"被视为兼容"readonly number[]"。
    这是TypeScript为了保持类型系统的实用性与安全性平衡而做的设计。

    详细解释
    1. 子类型关系(Subtyping)。
    在TypeScript中,以下赋值是合法的:
    const mutable: number[] = [1, 2, 3];
    const readonlyMutable: readonly number[] = mutable; // 允许!
    因为"readonly number[]"只承诺“我不会修改这个数组”,而mutable数组完全满足这个承诺
    (即使它自己有能力修改【即mutable可修改数组】,但作为readonlyMutable使用时不会这么做【即readonlyMutable是只读的,但mutable修改后,也会同步给readonlyMutable,因为它们指向同一个数组】)。
    这符合Liskov替换原则:子类型(可变数组)可以安全地用于需要父类型(只读数组)的地方。
    因此,在类型检查中:
    number[] extends readonly number[] → 成立(true)。
    2. 应用到你的"ElementType<T>"类型。
    type ElementType<T> = T extends readonly (infer E)[] ? E : T;
    当你传入一个普通数组:
    type R = ElementType<number[]>;
    TypeScript会判断:
    number[] extends readonly (infer E)[] 是否成立?
    由于"number[]"是"readonly number[]"的子类型,条件成立,于是:
    "infer E"成功推断出"E = number"。
    最终结果:R = number。
    所以即使你没写 readonly,也能正常工作!
    验证实验:
    // 普通数组
    type A = ElementType<number[]>; // number。
    type B = ElementType<string[]>; // string。
    // 只读数组
    type C = ElementType<readonly boolean[]>; // boolean。
    // 非数组类型
    type D = ElementType<string>; // string(走else分支)。
    全部按预期工作!

    那如果反过来呢?只写"T[]"能匹配"readonly T[]"吗?
    答案:不能!
    type ElementType<T> = T extends (infer E)[] ? E : T;
    type X = ElementType<readonly number[]>; // readonly number[]。不匹配!
    因为:
    "readonly number[]"不能赋值给"number[]"(否则可能意外修改只读数据)。
    所以:
    readonly T[] extends T[] → 不成立。
    条件失败,返回原类型"readonly number[]"。

    记住:在TypeScript中,“只读”是一种更宽泛(supertype)的约束,可变类型可以安全地视为只读类型使用。
    */

  • 提取类的构造函数参数(用于依赖注入等场景)。

    type ConstructorArgs<T> = T extends new (args: infer A) => any ? A : never;

    class User {
    constructor(name: string, age: number) {}
    }

    type Args = ConstructorArgs<typeof User>; // [string, number]。


  • infer的工作原理(关键点):

  • 只能用在条件类型的extends右侧。

    // 错误!不能在其他地方用infer。
    type Bad<T> = infer U; // SyntaxError。

  • 推断发生在类型匹配成功时。

    只有当T extends XXX成立,infer才会尝试从T的结构中“反向解析”出U。

    可以多次使用infer提取多个部分:

    type FuncParts<T> = T extends (infer A) => infer B ? [A, B] : never;
    type F = (x: number) => string;
    type P = FuncParts<F>; // [number, string]。

  • 支持嵌套和复杂结构。

    type DeepUnwrap<T> = T extends Promise<infer U> ? DeepUnwrap<U> : T;

    type X = DeepUnwrap<Promise<Promise<string>>>; // string。


  • 实际应用场景:

    场景工具类型示例
    获取函数返回值。 ReturnType<T>。
    获取函数参数。 Parameters<T>。
    获取构造函数参数。 ConstructorParameters<T>。
    解包Promise。 Awaited<T>(TS 4.5+ 内置)。
    提取数组/元组元素。 自定义ElementOf<T>。
    实现高级类型操作。 如DeepPartial<T>,Flatten<T>。

    常见误区:

  • infer是“声明变量”。

    正确理解:infer是在类型匹配过程中动态推导并绑定类型,不是静态声明。

  • infer可以提取任意位置的类型。

    实际:只能从满足结构匹配的类型中提取。

    例如:

    type Test<T> = T extends { data: infer D } ? D : never;
    type R = Test<{ data: number }>; // number。
    type R2 = Test<string>; // never(不匹配结构)。

  • infer总是返回联合类型。

    实际:取决于上下文。

    例如对函数重载的处理较复杂,但一般情况下按结构精确提取。


  • 总结:

    关键点说明
    作用 在条件类型中“提取”并捕获子类型。
    位置 仅限T extends … ? … : …的…(即extends右侧)。
    本质 类型级别的模式匹配与解构。
    用途 实现高级工具类型(如ReturnType、Parameters等)。
    限制 依赖结构匹配,无法提取不满足条件的类型。

    2.9 分布式条件类型

    type ToArray1<Type> = Type extends any ? Type[] : never;
    type ToArray2<Type> = Type extends { message: unknown } ? Type[] : never;

    interface A {
    message: string;
    }
    interface B {
    message: number;
    }

    type StrArrOrNumArr = ToArray1<string | number>;
    // type StrArrOrNumArr = string[] | number[]。

    type StrObjOrNumObj = ToArray2<A | B>;
    // type StrObjOrNumObj = A[] | B[]。

    问题:

    为何"StrArrOrNumArr"数据类型是string[] | number[]而不是(string | number)[]?

    "StrObjOrNumObj"也一样,为什么是A[] | B[]而不是(A | B)[]?

    注意:

    string[] | number[]与(string | number)[]是两个完全不同的类型。

    类型含义允许的值
    string[] | number[] 要么是纯字符串数组,要么是纯数字数组。 对:["a", "b"]对:[1, 2]错:["a", 1]
    (string | number)[] 数组元素可以是字符串或数字的混合。 对:["a", "b"]对:[1, 2]对:["a", 1, "b", 2]

    解答:

    因为ToArray1<T>和ToArray2<T>是一个分布式条件类型(distributive conditional type),当T是联合类型(如string | number)时,TypeScript会自动将条件类型分别应用到联合类型的每个成员上,然后再把结果union起来。

    ToArray1<string | number>
    // 等价于
    ToArray1<string> | ToArray1<number>
    // 即
    string[] | number[]

    而不是(string | number)[]。


    详细解释:

  • 什么是“分布式条件类型”?

    在TypeScript中,如果一个条件类型满足以下两个条件,它就是“分布式的”:

    • 形如T extends U ? X : Y(这不是条件)。

    • T是一个类型参数(generic type parameter)。

    • T被直接用在extends左侧(没有被包裹在其他结构中)。

    type ToArray<Type> = Type extends any ? Type[] : never;
    // Type是泛型参数,直接出现在extends左侧 → 分布式!

  • 分布式行为:自动拆解联合类型。

    当传入联合类型A | B | C时,分布式条件类型会:

    F<A | B>F<A> | F<B>

  • 为什么不是(string | number)[]?

    因为(string | number)[]表示“一个数组,元素可以是string或number”,而上述ToArray1和ToArray2的逻辑是:“把每个类型T变成T[]”。

    对联合类型string | number,它理解为:“要么是string类型 → 变成string[],要么是number类型 → 变成number[]”。

    所以结果是两个数组类型的联合,而不是一个混合元素数组类型。

  • 验证实验:

    type ToArray<Type> = Type extends any ? Type[] : never;

    type Result = ToArray<string | number>;
    // Hover查看类型:string[] | number[]。

    // 手动展开:
    type Manual = ToArray<string> | ToArray<number>; // string[] | number[]。
    // Result和Manual完全等价!


    如何得到 (string | number)[]?

    如果你不希望条件类型分布(即想把联合类型当作一个整体处理),你需要打破分布式条件类型的规则。

    • 方法:用方括号[]包裹类型参数。

      type ToArrayNonDistributive<Type> = [Type] extends [any] ? Type[] : never;

      type Result2 = ToArrayNonDistributive<string | number>;
      // 结果:(string | number)[]。

    • 原理:

      • [Type] extends [any]中,Type被包裹在元组中,不再是“裸类型参数”。

      • 因此不触发分布式行为。

        (不满足条件:T被直接用在extends左侧(没有被包裹在其他结构中))。

      • 整个string | number被当作一个整体传入,结果就是(string | number)[]。


    实际意义:

    • 分布式条件类型是很多内置工具类型的基础,例如:

      type Exclude<T, U> = T extends U ? never : T;
      type E = Exclude<"a" | "b" | "c", "a">; // "b" | "c"。

      正是靠分布式行为,才能逐个过滤联合类型成员。

    • 如果你不小心写出了分布式条件类型,但本意是整体处理,就会出现“意外拆解”的问题。


    最佳实践建议:

    目标写法
    对联合类型每个成员分别处理(如Exclude, Extract)。 用裸类型参数:T extends …。
    把联合类型当作一个整体。 包裹起来:[T] extends […]或{ x: T } extends { x: … }。
    赞(0)
    未经允许不得转载:171主机测评 » TypeScript学习笔记(Part—2)
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址