TypeScript泛型约束与条件类型实战:业务场景中的高级类型应用教程

TypeScript的泛型系统不仅限于Array<T>这类基础用法。在实际业务开发中,泛型约束、条件类型和映射类型组合使用,能够实现编译期的类型安全校验,减少运行时错误。本文通过表单验证、API请求封装和状态管理三个实测场景,讲解TypeScript高级类型技巧。

泛型约束:API请求响应类型安全封装

封装HTTP请求时,不同接口返回的数据结构不同。通过泛型约束实现调用方自动推导返回类型:

// 定义API响应基础结构
interface ApiResponse<T> {
    code: number;
    message: string;
    data: T;
}

// 业务数据类型
interface User {
    id: number;
    name: string;
    email: string;
}

interface Product {
    id: number;
    name: string;
    price: number;
}

// 泛型请求函数
async function request<T>(
    url: string,
    options?: RequestInit
): Promise<ApiResponse<T>> {
    const response = await fetch(url, options);
    const result: ApiResponse<T> = await response.json();
    
    if (result.code !== 0) {
        throw new Error(result.message);
    }
    
    return result;
}

// 调用时自动推导返回类型
const userRes = await request<User>('/api/user/1');
userRes.data.name;    // string类型,编译器自动推导
userRes.data.price;   // 编译错误:User上不存在price属性

const productRes = await request<Product>('/api/product/1');
productRes.data.price;   // number类型

泛型<T>在函数调用时由参数显式传入,编译器据此推导data字段的具体类型,访问不存在的属性会在编译期报错。

条件类型:动态表单字段类型推导

后台管理系统中,表单字段配置驱动渲染。不同字段类型对应不同的验证规则和值类型。使用条件类型实现字段配置到表单值的类型映射:

// 字段类型定义
type FieldType = 'text' | 'number' | 'select' | 'date' | 'switch';

// 条件类型:根据字段类型推导值类型
type FieldValue<T extends FieldType> = 
    T extends 'text' ? string :
    T extends 'number' ? number :
    T extends 'select' ? string :
    T extends 'date' ? string :  // ISO日期字符串
    T extends 'switch' ? boolean :
    never;

// 字段配置类型
interface FieldConfig<T extends FieldType> {
    name: string;
    label: string;
    type: T;
    required: boolean;
    options?: { label: string; value: string }[];  // select类型专用
    rules?: (value: FieldValue<T>) => string | true;
}

// 表单值类型:自动收集所有字段值
type FormValues<C extends FieldConfig<any>[]> = {
    [K in C[number]['name']]: 
        C[number] extends FieldConfig<infer F> 
            ? FieldValue<F> 
            : never
};

// 实际使用
const fields = [
    { name: 'username', label: '用户名', type: 'text' as const, required: true },
    { name: 'age', label: '年龄', type: 'number' as const, required: false },
    { name: 'vip', label: 'VIP用户', type: 'switch' as const, required: true }
] satisfies FieldConfig<any>[];

// formValues的类型自动推导为:
// { username: string; age: number; vip: boolean }
type FormType = FormValues<typeof fields>;

const formValues: FormType = {
    username: 'admin',  // string
    age: 25,            // number
    vip: true           // boolean
};
// formValues.username = 123;  // 编译错误:不能将number赋值给string

satisfies关键字(TypeScript 4.9+)用于校验字段配置符合FieldConfig结构,同时保留字面量类型信息。条件类型FieldValue根据字段类型映射到对应的值类型,FormValues将配置数组转换为键值对类型。

映射类型:DTO与实体类型自动转换

前后端数据交互中,后端返回snake_case,前端使用camelCase。手动维护两套类型容易遗漏。通过映射类型自动生成转换类型:

// 后端返回的DTO(snake_case)
interface UserDTO {
    user_id: number;
    user_name: string;
    created_at: string;
    is_active: boolean;
}

// snake_case -> camelCase 字符串类型转换
type SnakeToCamel<S extends string> =
    S extends `${infer Head}_${infer Tail}`
        ? `${Head}${Capitalize<SnakeToCamel<Tail>>}`
        : S;

// 映射类型:将DTO所有键转为camelCase
type CamelCase<T> = {
    [K in keyof T as SnakeToCamel<string & K>]: T[K]
};

// 自动生成前端Entity类型
type UserEntity = CamelCase<UserDTO>;
// 等价于:
// {
//     userId: number;
//     userName: string;
//     createdAt: string;
//     isActive: boolean;
// }

// 通用转换函数
function toCamelCase<T extends Record<string, any>>(
    dto: T
): CamelCase<T> {
    const result: any = {};
    for (const [key, value] of Object.entries(dto)) {
        const camelKey = key.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
        result[camelKey] = value;
    }
    return result;
}

const dto: UserDTO = { user_id: 1, user_name: 'admin', created_at: '2026-08-05', is_active: true };
const entity = toCamelCase(dto);
entity.userId;      // number - 类型安全
entity.userName;    // string

模板字面量类型:路由参数类型安全

Vue Router或React Router中,动态路由参数通过字符串拼接构造。模板字面量类型可在编译期校验路由格式:

// 定义路由模式
type RoutePattern = 
    | `/users` 
    | `/users/${number}` 
    | `/posts/${number}/comments`
    | `/posts/${number}/edit`;

// 类型安全的路由生成函数
function createRoute<P extends RoutePattern>(path: P): P {
    return path;
}

// 编译期校验
const r1 = createRoute('/users');              // OK
const r2 = createRoute('/users/123');          // OK
const r3 = createRoute('/posts/45/edit');      // OK
const r4 = createRoute('/users/abc');          // 编译错误:abc不是number
const r5 = createRoute('/unknown');            // 编译错误:不在RoutePattern中

类型收窄与运行时校验结合

TypeScript的类型在运行时被擦除。使用zod在运行时校验数据结构,同时自动推导TypeScript类型:

import { z } from 'zod';

// 定义Schema
const UserSchema = z.object({
    id: z.number(),
    name: z.string().min(1).max(50),
    email: z.string().email(),
    age: z.number().int().min(0).max(150).optional(),
    role: z.enum(['admin', 'user', 'guest'])
});

// 自动推导类型,无需interface
type User = z.infer<typeof UserSchema>;

// 运行时校验
function parseUser(data: unknown): User {
    return UserSchema.parse(data);  // 校验失败抛出ZodError
}

// 安全解析
function safeParseUser(data: unknown) {
    const result = UserSchema.safeParse(data);
    if (result.success) {
        result.data;    // User类型,类型安全
        return result.data;
    } else {
        result.error;   // ZodError,包含详细错误信息
        return null;
    }
}

z.infer从Schema自动推导TypeScript类型,保证运行时校验规则与编译期类型定义始终一致,避免定义与校验逻辑脱节。

原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/typescript-fan-xing-yue-shu-yu-tiao-jian-lei-xing-shi-zhan/

(0)
小编小编
上一篇 1天前
下一篇 1天前

相关推荐