最近掌握的高级TypeScript清单

786 阅读3分钟

src=http___pic1.win4000.com_wallpaper_2020-07-22_5f17fe7fe9959.jpg&refer=http___pic1.win4000 (1).webp

TypeScript 作为 JavaScript 的强大超集,彻底改变了开发者构建可扩展、可维护应用程序的方式。虽然基础的类型系统已经能确保代码更安全,但 TypeScript 的高级特性(如泛型、工具类型、映射类型和类型约束)将开发效率和类型安全性提升到了新的高度。本文将深入探讨这些高级概念,帮助你掌握 TypeScript,从而构建更健壮的应用程序。

1. 使用特定类型进行约束

如果你使用数组(Type[])作为泛型类型,它会天然保证一些属性(如 length),因为 JavaScript 中的所有数组都具备这一属性。

示例:

function getArrayLength<T>(arr: T[]): number {
    return arr.length; // 可以安全访问 `length`
}

const numbers = [1, 2, 3];
console.log(getArrayLength(numbers)); // 输出:3

const strings = ["a", "b", "c"];
console.log(getArrayLength(strings)); // 输出:3

这里,约束 T[] 确保了输入是一个数组,而数组总是有 length 属性。


2. 使用接口和 extends 进行约束

当处理可能包含或不包含某些属性的类型时,你可以使用接口来强制约束。例如,如果你需要确保某个类型具有 length 属性,可以定义一个接口并通过 extends 约束泛型类型。

示例:

interface HasLength {
    length: number;
}

function logWithLength<T extends HasLength>(value: T): void {
    console.log(`Length: ${value.length}`);
}

logWithLength("Hello, TypeScript!"); // 有效,因为字符串有 `length`
// 输出:Length: 17

logWithLength([1, 2, 3, 4]); // 有效,因为数组有 `length`
// 输出:Length: 4

logWithLength({ length: 5, name: "Example" }); // 有效,因为对象有 `length`
// 输出:Length: 5

// logWithLength(42); // 错误:数字没有 `length`

  • 特定类型约束(1) :将类型限制为数组,确保数组特有的属性(如 length)。
  • 接口与 extends:为任何类型添加自定义约束,确保其包含所需的属性或结构。

这种灵活性让你能够处理各种场景,同时保持 TypeScript 代码的健壮性和类型安全性。


3. 高级工具类型

TypeScript 提供了内置的工具类型来简化类型操作。

  • Partial:使所有属性变为可选。

    interface Props {
        id: string;
        name: string;
    }
    type PartialProps = Partial<Props>;
    
    
  • Readonly:使所有属性变为只读。

    type ReadonlyProps = Readonly<Props>;
    
    
  • Pick:从类型中提取特定属性。

    type PickedProps = Pick<Props, "id">;
    
    
  • Record:定义一个具有特定键及其关联值的类型。

    type RecordExample = Record<"a" | "b", number>;
    
    

4. 映射类型提升效率

映射类型可以动态转换现有类型,减少冗余。

示例:

type Keys = "x" | "y" | "z";
type Coordinates = { [K in Keys]: number };

这会创建一个对象类型,其键为 xy 和 z,值为 number 类型。


5. 索引签名用于动态结构

当对象具有动态键时,可以使用索引签名。

示例:

interface DynamicObject {
    [key: string]: string;
}
let obj: DynamicObject = {
    name: "Alice",
    age: "25", // 注意:虽然键是动态的,但值必须是字符串
};


6. 自定义工具类型实现

理解工具类型的内部实现可以加深你对 TypeScript 的掌握。

  • Readonly 实现:

    type MyReadonly<T> = {
        readonly [P in keyof T]: T[P];
    };
    
    
  • Partial 实现:

    type MyPartial<T> = {
        [P in keyof T]?: T[P];
    };
    
    
  • Pick 实现:

    type MyPick<T, K extends keyof T> = {
        [P in K]: T[P];
    };
    
    

7. 索引查询类型用于属性访问

索引查询类型(Indexed Access Types)允许你通过键名动态访问类型的属性类型。结合 keyof 操作符,你可以灵活地提取类型中的特定属性类型,甚至创建更复杂的类型操作。

基础示例

假设我们有一个对象类型 Props,我们可以通过键名直接访问其属性类型:

type Props = { a: number; b: string; c: boolean };

type TypeA = Props["a"]; // number
type TypeB = Props["b"]; // string
type TypeC = Props["c"]; // boolean

动态键名访问

基于上面的基础示例你可以使用 keyof 操作符结合索引查询类型,动态获取所有键的类型:

type AllPropTypes = Props[keyof Props]; // number | string | boolean

这里,keyof Props 会生成 "a" | "b" | "c",然后通过索引查询类型 Props[keyof Props] 提取出所有属性类型的联合类型。

嵌套对象访问

索引查询类型也适用于嵌套对象。例如:

type User = {
    id: number;
    name: string;
    address: {
        city: string;
        zipCode: string;
    };
};

type CityType = User["address"]["city"]; // string
type ZipCodeType = User["address"]["zipCode"]; // string

结合泛型使用

索引查询类型可以与泛型结合,创建更灵活的工具类型。例如,提取对象中某个键的类型:

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

const user = {
    name: "Alice",
    age: 25,
};

const name = getProperty(user, "name"); // string
const age = getProperty(user, "age"); // number

在这个例子中,T[K] 通过索引查询类型动态获取了 obj[key] 的类型。

动态创建新类型

你可以使用索引查询类型动态创建新类型。例如,提取对象中所有值的类型:

type ValueOf<T> = T[keyof T];

type UserValues = ValueOf<typeof user>; // string | number

这里,ValueOf<T> 会提取出 T 中所有值的联合类型。

复杂场景:提取函数返回值类型

索引查询类型还可以用于提取函数返回值类型。例如:

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

function exampleFunction(): number {
    return 42;
}

type ReturnType = FunctionReturnType<typeof exampleFunction>; // number

虽然这个例子使用了条件类型和 infer,但索引查询类型是其中的关键部分。

实际应用场景

  1. 动态表单字段类型提取
    假设你有一个表单配置对象,可以通过索引查询类型提取字段的类型:

    type FormConfig = {
        username: string;
        password: string;
        age: number;
    };
    
    type UsernameType = FormConfig["username"]; // string
    type AgeType = FormConfig["age"]; // number
    
    
  2. API 响应类型提取
    在处理 API 响应时,你可以动态提取嵌套数据的类型:

    type ApiResponse = {
        status: "success" | "error";
        data: {
            id: number;
            name: string;
        };
    };
    
    type DataType = ApiResponse["data"]; // { id: number; name: string }
    type StatusType = ApiResponse["status"]; // "success" | "error"
    
    
  3. 组件 Props 类型提取
    在 React 中,你可以提取组件 Props 的某个属性类型:

    type ButtonProps = {
        label: string;
        onClick: () => void;
        disabled: boolean;
    };
    
    type LabelType = ButtonProps["label"]; // string
    type OnClickType = ButtonProps["onClick"]; // () => void
    
    

8. TypeScript 中的函数兼容性

函数兼容性指的是一个函数类型能否赋值给另一个函数类型。这取决于参数数量、参数类型和返回值类型。我们逐一分析:

1. 参数数量

在 TypeScript 中,参数较少的函数可以赋值给参数较多的函数,因为接收函数会忽略多余的参数。

示例:

type F1 = (a: number) => void;
type F2 = (a: number, b: number) => void;

let f1: F1;
let f2: F2 = f1; // 兼容:f1 的参数比 f2 少

这里,f1 可以赋值给 f2,因为 f2 虽然需要更多参数,但 f1 不会使用它们。

实际示例(forEach):

数组的 forEach 方法接收一个回调函数,其签名为:

(value: string, index: number, array: string[]) => void;

然而,TypeScript 允许你在回调中省略未使用的参数,从而提升函数兼容性。

const arr = ['a', 'b', 'c'];

// 省略所有参数
arr.forEach(() => {
    console.log('未使用任何参数');
});

// 使用一个参数
arr.forEach((item) => {
    console.log(item);
});

// 使用所有参数
arr.forEach((item, index, array) => {
    console.log(`Item: ${item}, Index: ${index}, Array: ${array}`);
});

TypeScript 会根据上下文自动推断 itemindex 和 array 的类型,让你可以只使用需要的参数。

2. 参数类型

函数的参数类型必须兼容。TypeScript 使用结构类型系统,即通过参数类型的“形状”而非名称进行比较。

示例:

type F3 = (x: { name: string }) => void;
type F4 = (y: { name: string; age: number }) => void;

let f3: F3;
let f4: F4 = f3; // 兼容:f3 可以赋值给 f4

// 错误:f4 不能赋值给 f3
// let f3: F3 = f4;

这里,f3 可以赋值给 f4,因为 f3 的参数类型是 f4 参数类型的子集。

3. 返回值类型

函数的返回值类型也必须兼容。返回值类型更宽泛的函数可以赋值给返回值类型更具体的函数。

示例:

type F5 = () => string;
type F6 = () => string | number;

let f5: F5;
let f6: F6 = f5; // 兼容:f5 的返回值类型更具体

然而,反过来则会报错:

// 错误:f6 不能赋值给 f5
// let f5: F5 = f6;

函数兼容性关键点:

  • 参数较少的函数 → 参数较多的函数:参数较少的函数可以赋值给参数较多的函数。
  • 参数类型子集:参数类型为子集的函数可以赋值给参数类型为超集的函数。
  • 返回值类型更具体 → 返回值类型更宽泛:返回值类型更具体的函数可以赋值给返回值类型更宽泛的函数。

通过理解这些规则,你可以充分利用 TypeScript 的类型推断和结构类型系统,使代码既健壮又灵活。


结论

通过掌握这些 TypeScript 高级概念,你可以充分发挥这门语言的潜力。从定义可重用的泛型函数到利用工具类型,你可以构建高度可扩展、可维护且健壮的应用程序。无论你是在开发企业级系统还是小型项目,TypeScript 的高级特性都为你提供了现代开发中所需的工具,助你实现卓越。