如何让递归反序列化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
相关产品推荐
相关产品推荐

