TypeScript 您可能不知道的15个高级技巧

TypeScript 已成为许多开发人员的必备工具,它提供类型安全性和增强的开发人员体验。虽然大多数人都熟悉它的基本功能,但 TypeScript 有很多高级技术可以提高应用程序的类型安全性。本文深入探讨了 15 个鲜为人知的 TypeScript 提示和技巧,它们将扩展您的工具包,并可能重塑您进行 TypeScript 开发的方式。不要浪费任何时间,让我们开始吧!

1. 字符串文字插值类型

字符串文本类型功能强大,但您知道可以对它们进行插值吗?此功能允许基于其他类型的动态创建字符串文本类型。

type EventName<T extends string> = `${T}Changed`;
type UserEvent = EventName<"user">; // type UserEvent = "userChanged"

在处理事件系统或在整个代码库中创建一致的命名约定时,此技术特别有用。例如,你可以使用它来自动生成 getter 名称:

type Getter<T extends string> = `get${Capitalize<T>}`;
type UserGetter = Getter<"username">; // type UserGetter = "getUsername"

 2. 使用标记类型交集

标记类型提供了一种在 TypeScript 的结构类型系统中创建名义类型的方法。当您有多个不应互换的字符串或数字类型时,它们非常适合防止类型混合。

type UserId = string & { readonly brand: unique symbol };
type PostId = string & { readonly brand: unique symbol };

function createUserId(id: string): UserId {
    return id as UserId;
}

function createPostId(id: string): PostId {
    return id as PostId;
}

const userId = createUserId("user123");
const postId = createPostId("post456");

// This will cause a type error:
// const error = userId = postId;

此模式可确保即使 UserId 和 PostId 在后台都是字符串,它们也不会意外地混入您的代码中。

3. 带 Infer 的条件类型

条件类型中的 infer 关键字允许您从复杂类型中提取类型信息。它在处理函数、promise 或数组时特别有用。

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

type ResolvedType = UnpackPromise<Promise<string>>; // type ResolvedType = string
type NonPromiseType = UnpackPromise<number>; // type NonPromiseType = number

// Another practical example: extracting return types of functions
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

function fetchUser() { return { id: 1, name: "John" }; }
type User = ReturnType<typeof fetchUser>; // type User = { id: number; name: string; }

此技术允许强大的类型推理和操作,使您能够创建更灵活和可重用的类型定义。

4. 模板文字类型

模板文本类型将文本类型和字符串操作组合在一起,以创建基于字符串的强大类型约束。

type ColorVariant = "light" | "dark";
type Color = "red" | "green" | "blue";
type Theme = `${ColorVariant}-${Color}`;

// Theme is now equivalent to:
// "light-red" | "light-green" | "light-blue" | "dark-red" | "dark-green" | "dark-blue"

function setTheme(theme: Theme) {
    // Implementation
}

setTheme("light-red"); // OK
// setTheme("medium-purple"); // Error: Argument of type '"medium-purple"' is not assignable to parameter of type 'Theme'.

在使用 CSS-in-JS 库、API 路由定义或任何需要在类型级别强制执行特定字符串模式的场景时,此功能大放异彩。

5. 递归类型别名

递归类型别名允许您定义引用自身的类型。这在处理树状结构或嵌套数据时特别有用。

type JSONValue = 
    | string 
    | number 
    | boolean 
    | null 
    | JSONValue[] 
    | { [key: string]: JSONValue };

const data: JSONValue = {
    name: "John Doe",
    age: 30,
    isStudent: false,
    hobbies: ["reading", "cycling"],
    address: {
        street: "123 Main St",
        city: "Anytown",
        coordinates: [40.7128, -74.0060]
    }
};

此 JSONValue 类型准确表示任何有效的 JSON 结构,无论嵌套有多深。在使用 API、配置文件或涉及复杂嵌套数据结构的任何场景时,它非常宝贵。

前 5 个技巧只是 TypeScript 高级功能的皮毛。它们演示了 TypeScript 如何在复杂场景中提供强类型,从而提高代码可靠性和开发人员的工作效率。在下一节中,我们将探索更高级的概念,这些概念突破了 TypeScript 类型系统的界限。

 6. 可变元组类型

TypeScript 4.0 中引入的可变元组类型允许更灵活的元组操作。当使用采用可变数量参数的函数或需要动态组合元组时,它们特别有用。

type Concat<T extends unknown[], U extends unknown[]> = [...T, ...U];
type Result = Concat<[1, 2], [3, 4]>; // type Result = [1, 2, 3, 4]

function concat<T extends unknown[], U extends unknown[]>(arr1: T, arr2: U): Concat<T, U> {
    return [...arr1, ...arr2];
}

const result = concat([1, 2], [3, 4]); // result: [1, 2, 3, 4]

此功能支持对元组进行类型安全操作,这在使用返回或期望特定元组结构的 API 时非常有价值。

7. 通过 'as' 重新映射键

映射类型中的 as 子句允许您转换对象类型的键。这对于创建具有已修改属性名称的派生类型非常有用。

type Getters<T> = {
    [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K]
};

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

type PersonGetters = Getters<Person>;
// Equivalent to:
// {
//     getName: () => string;
//     getAge: () => number;
// }

const person: Person = { name: "Alice", age: 30 };
const getters: PersonGetters = {
    getName: () => person.name,
    getAge: () => person.age
};

console.log(getters.getName()); // Output: "Alice"

当为需要特定命名约定的框架或库生成派生类型时,此技术特别有用。

8. 类型位置的 const 断言

Const 断言可用于从数组和对象创建更具体的文字类型。当您希望将运行时值用作类型时,这尤其有用。

const colors = ["red", "green", "blue"] as const;
type Color = typeof colors[number]; // type Color = "red" | "green" | "blue"

function paintShape(color: Color) {
    // Implementation
}

paintShape("red"); // OK
// paintShape("yellow"); // Error: Argument of type '"yellow"' is not assignable to parameter of type 'Color'.

// Another example with an object
const config = {
    endpoint: "https://api.example.com",
    timeout: 3000
} as const;

type Config = typeof config;
// Equivalent to:
// {
//     readonly endpoint: "https://api.example.com";
//     readonly timeout: 3000;
// }

此功能允许您为运行时值和类型信息维护单一事实来源,从而减少类型和实际数据之间不一致的可能性。

9. “never”的歧视工会

可区分联合是模拟互斥状态的有效方法。结合 never 类型,它们可以提供详尽的模式匹配和改进的类型安全性。

type Shape = 
    | { kind: "circle"; radius: number }
    | { kind: "square"; sideLength: number }
    | { kind: "triangle"; base: number; height: number };

function area(shape: Shape): number {
    switch (shape.kind) {
        case "circle":
            return Math.PI * shape.radius ** 2;
        case "square":
            return shape.sideLength ** 2;
        case "triangle":
            return 0.5 * shape.base * shape.height;
        default:
            const _exhaustiveCheck: never = shape;
            return _exhaustiveCheck;
    }
}

在这个例子中,如果我们要添加新的形状类型但忘记更新 area 函数,TypeScript 会给我们一个编译时错误。这可确保处理所有情况,并使重构更加安全。

10. 使用键过滤的映射类型

映射类型可以与条件类型结合使用,以根据其值类型筛选对象键。这允许强大的类型转换。

type PickByType<T, U> = {
    [P in keyof T as T[P] extends U ? P : never]: T[P]
};

interface Example {
    a: string;
    b: number;
    c: boolean;
    d: string;
}

type StringProps = PickByType<Example, string>;
// Equivalent to:
// {
//     a: string;
//     d: string;
// }

// Practical use case: creating a type for form field values
interface FormFields {
    name: string;
    email: string;
    age: number;
    newsletter: boolean;
}

type StringFields = PickByType<FormFields, string>;
// Equivalent to:
// {
//     name: string;
//     email: string;
// }

function validateStringFields(fields: StringFields) {
    // Implementation
}

validateStringFields({ name: "John", email: "john@example.com" }); // OK
// validateStringFields({ name: "John", age: 30 }); // Error: Object literal may only specify known properties, and 'age' does not exist in type 'StringFields'.

当您需要根据对象属性的类型处理对象属性的子集时,例如在表单验证或数据转换方案中,此技术特别有用。

这额外的 5 个技巧展示了 TypeScript 的更多高级类型操作功能。它们演示了如何利用 TypeScript 的类型系统来创建高度具体和安全的类型,从而产生更健壮和自文档化的代码。在最后一节中,我们将探索更高级的概念,这些概念突破了 TypeScript 类型系统的可能性界限。

11. 使用泛型的类型安全事件发射器

创建类型安全的事件发射器可以显著提高事件驱动代码的可靠性。通过利用泛型,我们可以确保事件名称及其相应的数据类型始终同步。

type Listener<T> = (event: T) => void;

class TypedEventEmitter<EventMap extends Record<string, any>> {
    private listeners: { [K in keyof EventMap]?: Listener<EventMap[K]>[] } = {};

    on<K extends keyof EventMap>(event: K, listener: Listener<EventMap[K]>) {
        if (!this.listeners[event]) {
            this.listeners[event] = [];
        }
        this.listeners[event]!.push(listener);
    }

    emit<K extends keyof EventMap>(event: K, data: EventMap[K]) {
        this.listeners[event]?.forEach(listener => listener(data));
    }
}

// Usage
interface MyEvents {
    userLoggedIn: { userId: string; timestamp: number };
    dataLoaded: { items: string[] };
}

const emitter = new TypedEventEmitter<MyEvents>();

emitter.on("userLoggedIn", ({ userId, timestamp }) => {
    console.log(`User ${userId} logged in at ${timestamp}`);
});

emitter.emit("userLoggedIn", { userId: "123", timestamp: Date.now() }); // OK
// emitter.emit("userLoggedIn", { userId: "123" }); // Error: Property 'timestamp' is missing
// emitter.emit("invalidEvent", {}); // Error: Argument of type '"invalidEvent"' is not assignable to parameter of type 'keyof MyEvents'

此模式可确保事件驱动代码是类型安全的,从而防止因事件名称不匹配或数据结构不正确而出错。

12. 自引用类型

自引用类型在处理递归数据结构(例如树状对象或链表)时很有用。

type FileSystemObject = {
    name: string;
    size: number;
    isDirectory: boolean;
    children?: FileSystemObject[];
};

const fileSystem: FileSystemObject = {
    name: "root",
    size: 1024,
    isDirectory: true,
    children: [
        {
            name: "documents",
            size: 512,
            isDirectory: true,
            children: [
                { name: "report.pdf", size: 128, isDirectory: false },
                { name: "invoice.docx", size: 64, isDirectory: false }
            ]
        },
        { name: "image.jpg", size: 256, isDirectory: false }
    ]
};

function calculateTotalSize(fsObject: FileSystemObject): number {
    if (!fsObject.isDirectory) {
        return fsObject.size;
    }
    return fsObject.size + (fsObject.children?.reduce((total, child) => total + calculateTotalSize(child), 0) ?? 0);
}

console.log(calculateTotalSize(fileSystem)); // Outputs the total size of all files

此技术允许您对复杂的嵌套结构进行建模,同时在这些结构上的整个操作中保持类型安全。

13. 使用唯一符号的不透明类型

不透明类型提供了一种创建结构相似但被类型系统视为不同的类型的方法。这对于创建类型安全标识符或防止意外误用类似类型非常有用。

declare const brand: unique symbol;

type Brand<T, TBrand> = T & { readonly [brand]: TBrand };

type Email = Brand<string, "Email">;
type UserId = Brand<string, "UserId">;

function createEmail(email: string): Email {
    // In a real application, you'd validate the email here
    return email as Email;
}

function sendEmail(email: Email, message: string) {
    console.log(`Sending "${message}" to ${email}`);
}

const email = createEmail("user@example.com");
const userId = "12345" as UserId;

sendEmail(email, "Hello!"); // OK
// sendEmail(userId, "Hello!"); // Error: Argument of type 'UserId' is not assignable to parameter of type 'Email'

当使用不应互换的域特定类型时,即使它们共享相同的基础结构,此模式也特别有用。

14. 类型级整数序列

在类型级别创建整数序列对于更高级的类型操作非常有用,尤其是在使用特定长度的元组或数组时。

type BuildTuple<L extends number, T extends any[] = []> =
    T['length'] extends L ? T : BuildTuple<L, [...T, any]>;

type Range<F extends number, T extends number> = Exclude<BuildTuple<T>[number], BuildTuple<F>[number]>;

type NumRange = Range<2, 5>; // type NumRange = 2 | 3 | 4

function createArray<T, N extends number>(element: T, length: Range<1, 11>): T[] {
    return Array(length).fill(element);
}

const arr1 = createArray("hello", 5); // OK
// const arr2 = createArray("world", 0); // Error: Argument of type '0' is not assignable to parameter of type 'Range<1, 11>'
// const arr3 = createArray("!", 11); // Error: Argument of type '11' is not assignable to parameter of type 'Range<1, 11>'

15. 使用递归条件类型的类型安全深度部分

在处理复杂的嵌套对象时,具有 DeepPartial 类型通常很有用,该类型使所有属性都以递归方式可选。这可以使用递归条件类型来实现。

type DeepPartial<T> = T extends object ? {
    [P in keyof T]?: DeepPartial<T[P]>;
} : T;

interface NestedObject {
    a: {
        b: {
            c: number;
            d: string;
        };
        e: boolean;
    };
    f: string[];
}

type PartialNested = DeepPartial<NestedObject>;

// Usage
function updateNestedObject(obj: NestedObject, update: DeepPartial<NestedObject>): NestedObject {
    // Implementation (deep merge logic)
    return { ...obj, ...update } as NestedObject; // Simplified for brevity
}

const original: NestedObject = {
    a: { b: { c: 1, d: "hello" }, e: true },
    f: ["one", "two"]
};

const updated = updateNestedObject(original, {
    a: { b: { c: 2 } },
    f: ["three"]
});

console.log(updated);
// Output: { a: { b: { c: 2, d: "hello" }, e: true }, f: ["three"] }

此 DeepPartial 类型在处理复杂对象的部分更新时特别有用,例如在状态管理系统中或处理可能包含部分数据的 API 响应时。

结束

请记住,虽然这些高级功能很强大,但应谨慎使用。始终努力在代码库中保持清晰和简单,当这些高级技术在类型安全或开发人员体验方面提供明显优势时,请使用这些高级技术。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值