簡體   English   中英

映射類型和泛型函數輸入錯誤

[英]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的類型為FieldKeyvalue的類型為FieldInputs[FieldKey] ,它們有 50% 的機會不匹配。 盡管如此,編譯器並沒有警告您:它推斷FFieldKey (這是一個完全有效的事情),並且允許調用update()

update()的實現中,有一個警告,即FieldParsers[F]可能不是FieldParser<F> 如果FFieldKey ,則這種不匹配變得明顯。 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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM