You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何让递归反序列化JSON日期字符串的泛型函数类型安全?

实现类型安全的stringsToDates函数

现有stringsToDates函数可递归将值、对象或Promise中的ISO8601日期字符串转换为Date实例,但类型处理存在缺陷:调用时返回类型被推断为包含string | Date的松散类型,而非期望的目标业务对象类型;调整函数签名后还会出现返回值类型不匹配的错误。需要修改类型定义和函数实现,让stringsToDates<MyBusinessObject>(serializedObject)返回严格的MyBusinessObject类型。

问题代码分析

原代码的核心问题在于DateDeserialized<T>类型的定义:它将所有string类型都映射为string | Date,无法区分普通字符串和ISO日期字符串,导致返回类型不符合预期;此外函数实现中各分支的返回值无法被TypeScript自动推断为DateDeserialized<T>,引发类型错误。

原代码:

type IsoDateString = string
function isIsoDateString(value: unknown): value is IsoDateString {
    const isoDateFormat = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d*)?Z$/;
    if (value === null || value === undefined) {
        return false;
    }
    if (typeof value === 'string') {
        return isoDateFormat.test(value);
    }
    return false;
}

type DateDeserialized<T> =
    T extends string ? Date | T :
    T extends PromiseLike<infer U> ? PromiseLike<DateDeserialized<U>> :
    { [K in keyof T]: DateDeserialized<T[K]> }

function stringsToDates<T>(body: T): DateDeserialized<T> {
    if (body === null || body === undefined) {
        return body;
    } else if (body instanceof Promise) {
        return body.then(stringsToDates);
    } else if (Array.isArray(body)) {
        return body.map(stringsToDates);
    } else if (isIsoDateString(body)) {
        return new Date(body);
    } else if (typeof body !== 'object') {
        return body;
    } else {
        const bodyRecord = body;
        return Object.keys(bodyRecord).reduce((previous, current): Record<string, any> => {
            const currentValue = bodyRecord[current];
            if (isIsoDateString(currentValue)) {
                return { ...previous, [current]: new Date(currentValue) };
            } else {
                return { ...previous, [current]: stringsToDates(currentValue) };
            }
        }, Object.create({}));
    }
}

// 调用示例:返回类型不符合预期
type MyBusinessObject = { foo: string, bar: Date };
const input = { foo: 'example', bar: (new Date(0)).toJSON() }
const result = stringsToDates(input); 
// result类型被推断为 { foo: string | Date; bar: string | Date; }

解决方案

1. 定义序列化类型映射

首先定义Serialized<T>类型,描述目标类型T的JSON序列化形式:将T中的Date类型转为string(ISO日期格式),其他类型递归保持原有结构。

2. 调整函数泛型签名

修改stringsToDates的泛型参数为目标类型T,接受Serialized<T>类型的输入,返回T类型。通过unknown类型断言解决各分支返回值的类型匹配问题(因为TypeScript无法自动验证运行时类型转换的安全性)。

修改后的代码

type IsoDateString = string
function isIsoDateString(value: unknown): value is IsoDateString {
    const isoDateFormat = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d*)?Z$/;
    if (value === null || value === undefined) {
        return false;
    }
    if (typeof value === 'string') {
        return isoDateFormat.test(value);
    }
    return false;
}

// 定义目标类型T的序列化形式
type Serialized<T> =
    T extends Date ? string :
    T extends PromiseLike<infer U> ? PromiseLike<Serialized<U>> :
    T extends Array<infer U> ? Array<Serialized<U>> :
    T extends object ? { [K in keyof T]: Serialized<T[K]> } :
    T;

function stringsToDates<T>(body: Serialized<T>): T {
    if (body === null || body === undefined) {
        return body as T;
    } else if (body instanceof Promise) {
        return body.then(stringsToDates) as unknown as T;
    } else if (Array.isArray(body)) {
        return body.map(stringsToDates) as unknown as T;
    } else if (isIsoDateString(body)) {
        return new Date(body) as unknown as T;
    } else if (typeof body !== 'object') {
        return body as T;
    } else {
        const bodyRecord = body as Record<string, unknown>;
        return Object.keys(bodyRecord).reduce((previous, current) => {
            const currentValue = bodyRecord[current];
            if (isIsoDateString(currentValue)) {
                return { ...previous, [current]: new Date(currentValue) };
            } else {
                return { ...previous, [current]: stringsToDates(currentValue as Serialized<unknown>) };
            }
        }, Object.create({})) as unknown as T;
    }
}

// 调用示例:返回类型为MyBusinessObject
type MyBusinessObject = { foo: string, bar: Date };
const input = { foo: 'example', bar: (new Date(0)).toJSON() } as Serialized<MyBusinessObject>;
const result = stringsToDates<MyBusinessObject>(input); 
// result类型:MyBusinessObject

说明

  • Serialized<T>明确了目标类型和序列化输入的对应关系,TypeScript会验证输入是否符合目标类型的序列化结构。
  • 函数实现中的as unknown as T断言是必要的:由于TypeScript无法在类型系统中区分普通字符串和ISO日期字符串,只能通过运行时逻辑保证转换正确性,因此需要手动告诉TypeScript返回值符合目标类型。
  • 调用时指定泛型参数<MyBusinessObject>,即可让result获得严格的目标类型。

内容的提问来源于stack exchange,提问作者Jacob Tucker

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.02 02:45:18