[英]How to make type inference work when generic type can be recursively nested
[英]How can I make a generic function that recursively deserializes JSON with nested datestrings type safe?
下面的代碼定義了一個 function stringsToDates
,它將采用原語 object 或 Promise 並遞歸地將它找到的任何 ISO8601 日期字符串(序列化 JSON)轉換為實例化的 Date 對象。 我怎樣才能使這樣的 function 類型安全,以便我可以調用const result = stringsToDates<MyBusinessObject>(serializedObject)
並且result
是MyBusinessObject
類型? 我已經驗證了功能(如果需要,可以包括測試套件),但在打字方面苦苦掙扎。
如果我用注釋行替換 function 簽名,一些返回語句會拋出類型錯誤,例如Type 'Promise<any>' is not assignable to type 'DateDeserialized<T>'.
更多示例位於本節底部的 TS Playground 中。
代碼片段:
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;
console.log(bodyRecord)
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({}));
}
}
// Example invocation
type MyBusinessObject = { foo: string, bar: Date };
const input = { foo: 'example', bar: (new Date(0)).toJSON() }
const result = stringsToDates(input);
/* const result: {
foo: string | Date;
bar: string | Date;
} */
由@jcalz 提供的新 TS 游樂場
TypeScript 類型檢查器目前無法對依賴於尚未指定的通用類型參數的條件類型進行大量分析。 它實質上推遲了對此類類型的評估,因此將它們視為不透明的。 它不知道什么值可以或不可以分配給這些類型,並且它通過發出關於可分配性的警告而在安全方面犯了錯誤。
DateDeserialized<T>
類型是條件類型,在stringsToDates()
的主體內,類型T
是泛型類型參數,編譯器不知道它到底是什么。 所以DateDeserialized<T>
對編譯器來說是不透明的,當你返回類似body
(類型T
)的東西時,編譯器將它與DateDeserialized<T>
進行比較,無法驗證它是否有效,並發出類似Type 'T' is not assignable to type 'DateDeserialized<T>'
。 其他返回語句也是如此。
將此與stringsToDates stringsToDates()
的行為進行比較。 你打電話時
const result: { foo: string | Date, bar: string | Date } =
stringsToDates({ foo: 'example', bar: (new Date(0)).toJSON() });
編譯器推斷T
已指定為特定類型{foo: string; bar: string}
{foo: string; bar: string}
,因此返回類型是DateDeserialized<{foo: string; bar: string}>
DateDeserialized<{foo: string; bar: string}>
,不再通用。 它將類型完全評估為{ foo: string | Date; bar: string | Date; }
{ foo: string | Date; bar: string | Date; }
{ foo: string | Date; bar: string | Date; }
因此對result
的賦值成功。
所以你的代碼的問題是編譯器無法分析泛型T
的DateDeserialized<T>
。
現在,您可能期望通過顯式檢查類型T
的body
,然后編譯器將能夠理解關於T
的更具體的內容,以便DateDeserialized<T>
類型變得具體,它可以評估。 畢竟,如果body === null || body === undefined
body === null || body === undefined
是真的,那不就是說T
是null | undefined
null | undefined
,然后返回類型將是特定類型DateDeserialized<null | undefined>
DateDeserialized<null | undefined>
就是null | undefined
null | undefined
?
好吧,不。 問題是在檢查body === null || body === undefined
body === null || body === undefined
可以直接告訴你一些關於body
的事情,它不會對類型參數T
本身做任何事情。 在那次檢查之后,我們對T
的了解是null
和undefined
在它的域中; 它可能是string | null
string | null
或{a: number} | undefined
{a: number} | undefined
或unknown
或任何東西。 而這種復雜性並沒有被編譯器很好地建模,所以它仍然放棄了。
不過,如果它能做得更好,那就太好了。 並且有各種關於此的開放功能請求。 這里最適用的可能是microsoft/TypeScript#33912 ,它要求使用控制流分析的某種方式來獲取有關return
語句中條件類型的更多有用信息。 如果這得到實施,它可能會解決或至少改善這種情況。 但現在我們必須解決它。
最簡單的解決方法是接受編譯器毫無准備地在廣泛處理通用條件類型的 function 的主體內進行任何有用的類型檢查,並采取措施通過類型斷言等技術來抑制錯誤。
在這種情況下,我通常會將有問題的 function 變成具有單個調用簽名的重載function。 重載通常用於多個不同的調用簽名,但它們也允許在調用者查看的 function 和實現者查看的 function 之間設置一個屏障,后者相對於前者進行相當松散的檢查。
在您的示例中,我只是將通用條件版本移至調用簽名,並在實現簽名中使用“關閉類型檢查” any
類型:
// call signature
function stringsToDates<T>(body: T): DateDeserialized<T>;
// implementation
function stringsToDates(body: any) {
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;
console.log(bodyRecord)
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({}));
}
}
現在沒有錯誤了。
請注意,沒有錯誤並不意味着 function 主體中的類型安全。此解決方法的全部意義在於抑制錯誤,因為編譯器是無與倫比的。 這意味着如果你想在 function 的主體內部實現類型安全,它必須由你而不是編譯器來驗證。 因此,我建議您對實現進行雙重和三次檢查,以確保返回的值確實屬於DateDeserialized<T>
類型,或者至少接近到有用的程度。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.