[英]Mapped types & generic function typing error
我寫了一個相當簡單的基於映射類型的代碼,出於某種原因不想進行類型檢查。
首先,定義輸入和輸出:
interface Validated<T> {
valid: boolean;
value: T;
}
interface FieldInputs {
name: string;
price: number;
}
interface ParsedFields {
name: Validated<string>;
price: Validated<number>;
}
定義解析器類型和解析器映射:
type FieldKey = keyof FieldInputs & keyof ParsedFields;
type FieldParser<F extends FieldKey> = (value?: FieldInputs[F]) => ParsedFields[F];
type FieldParsers = {
[F in FieldKey]: FieldParser<F>;
};
declare let fieldParsers: FieldParsers;
現在這個非常簡單的通用函數無法進行類型檢查:
function update<F extends FieldKey>(field: F, value: FieldInputs[F]) {
const parser: FieldParser<F> = fieldParsers[field];
parser.apply(value);
}
給出以下錯誤( --strictFunctionTypes
):
Type 'FieldParsers[F]' is not assignable to type 'FieldParser<F>'.
Type 'FieldParser<"name"> | FieldParser<"price">' is not assignable to type 'FieldParser<F>'.
Type 'FieldParser<"name">' is not assignable to type 'FieldParser<F>'.
Types of parameters 'value' and 'value' are incompatible.
Type 'FieldInputs[F]' is not assignable to type 'string'.
Type 'string | number' is not assignable to type 'string'.
Type 'number' is not assignable to type 'string'.
我錯過了什么?
編譯器正在保護您免受不太可能發生的事情的影響,您需要決定如何解決它(劇透警告:使用類型斷言)
想象一下,如果我這樣做:
const field = Math.random() < 0.5 ? "name" : "price";
const value = Math.random() < 0.5 ? "Widget" : 9.95;
update(field, value); // no error
在這種情況下, field
的類型為FieldKey
, value
的類型為FieldInputs[FieldKey]
,它們有 50% 的機會不匹配。 盡管如此,編譯器並沒有警告您:它推斷F
是FieldKey
(這是一個完全有效的事情),並且允許調用update()
。
在update()
的實現中,有一個警告,即FieldParsers[F]
可能不是FieldParser<F>
。 如果F
是FieldKey
,則這種不匹配變得明顯。 FieldParsers[F]
將是FieldParser<'name'> | FieldParser<'price'>
FieldParser<'name'> | FieldParser<'price'>
,但FieldParser<F>
是FieldParser<'name' | 'price'>
FieldParser<'name' | 'price'>
。 前者是要么東西,解析string
或者一些解析number
。 后者是一些解析無論是string
或者number
。 它們不一樣(由於使用--strictFunctionTypes
啟用的函數參數的逆變)。 當上面的代碼最終調用update("name", 9.95)
並且您嘗試使用string
解析器解析number
時,這些類型之間的區別就暴露了。 你想要一個FieldParser<F>
,但你所擁有的只是一個FieldParsers[F]
。
現在備份,有人可能會玩這樣的游戲,其中F
是值的聯合嗎? 如果是這樣,那么您可能希望更改update()
定義,以明確禁止F
不是單個字符串文字。 就像是...
type NotAUnion<T, U = T> =
U extends any ? [T] extends [U] ? T : never : never;
declare function update<F extends FieldKey>(
field: F & NotAUnion<F>,
value: FieldInputs[F]
);
但這可能是矯枉過正,它仍然沒有解決update()
實現中的警告。 編譯器不夠聰明,無法理解F
的值是單個字符串文字值,並且您所做的事情是安全的。
為了消除該錯誤,您可能需要執行類型斷言。 要么您知道沒有人可能會通過將F
加寬到FieldKey
來故意朝自己的腳FieldKey
,要么您使用NotAUnion
東西阻止了調用者這樣做。 無論哪種情況,您都可以告訴編譯器您知道fieldParsers[field]
將是一個有效的FieldParser<F>
:
function update<F extends FieldKey>(field, value: FieldInputs[F]) {
const parser = fieldParsers[field] as FieldParser<F>; // okay
parser.apply(value);
}
所以這有效。 希望有幫助。 祝你好運!
我想你可能對這些類型有點過分了。 簡化它們實際上可以解決您的問題。
讓我們從查看FieldParser
的類型定義FieldParser
:
type FieldParser<F extends FieldKey> = (value?: FieldInputs[F]) => ParsedFields[F];
它真正做的就是接受一個值並返回一個相同類型的Validated
對象。 我們可以將其簡化為:
type FieldParser<T> = (value?: T) => Validated<T>;
這不僅提高了復雜性,而且還大大提高了類型的可讀性。
但是請注意,這確實意味着我們已經失去了對FieldParser
的限制,即它只能與來自FieldKey
鍵一起使用。 但實際上,如果您考慮“字段解析器”的通用概念,它應該是通用的,正如我們稍后將看到的,這並不意味着您的使用代碼變得不那么嚴格。
然后我們還可以將FieldParsers
構建為泛型類型
type FieldParsers<T> = {
[K in keyof T]: FieldParser<K>;
}
然后其余的代碼可以使用那些沒有問題的:
interface MyFieldInputs {
name: string;
price: number;
}
declare let fieldParsers: FieldParsers<MyFieldInputs>;
function update<T extends keyof MyFieldInputs>(field: T, value: MyFieldInputs[T]) {
const parser = fieldParsers[field];
parser.apply(value);
}
然而,我們還可以做得更好。 您仍然必須在這里使用parser.apply(value)
,而實際上您應該能夠簡單地調用parser(value)
。
讓我們將泛型更進一步,與其硬編碼update
函數以使用我們在函數之前定義的特定fieldParsers
變量, fieldParsers
使用一個函數來構建更新函數。
function buildUpdate<TInputs>(parsers: FieldParsers<TInputs>) {
return function update<T extends keyof TInputs>(field: T, value: TInputs[T]) {
const parser = parsers[field];
parser(value);
}
}
通過這樣做,我們可以輕松地將所有類型聯系在一起,Typescript 將簡單地接受(和類型檢查)調用parser(value)
。
所以現在,把它們放在一起,你最終得到:
interface Validated<T> {
valid: boolean;
value: T;
}
/**
* Generic field validator
*/
type FieldParser<T> = (value?: T) => Validated<T>;
/**
* Generic set of field validators for a specific set of field types
*/
type FieldParsers<T> = {
[K in keyof T]: FieldParser<T[K]>
}
function buildUpdate<TInputs>(parsers: FieldParsers<TInputs>) {
return function update<T extends keyof TInputs>(field: T, value: TInputs[T]) {
const parser = parsers[field];
parser(value);
}
}
您可以通過執行以下操作來使用它:
interface MyFieldInputs {
name: string;
price: number;
}
declare let fieldParsers: FieldParsers<MyFieldInputs>;
const update = buildUpdate(fieldParsers);
update('name', 'new name'); // Fully type checked
update('name', 5); // ERROR
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.