【问题标题】:Mapped types & generic function typing error映射类型和通用函数类型错误
【发布时间】:2020-11-18 22:27:41
【问题描述】:

我写了一个相当简单的基于映射类型的代码,由于某种原因不想进行类型检查。

首先,定义输入和输出:

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'.

我错过了什么?

Playground Link

【问题讨论】:

  • 游乐场没有错误
  • @Niladri 你需要启用--strictFunctionTypes - 链接中似乎没有共享游乐场选项。
  • 这是因为它不允许双变量分配,您将联合类型 "string"|number 分配给 fieldParsers[field],它仅属于字符串类型。在这种情况下,您只能将基本/超类型分配给派生类型。即不允许使用 number = string
  • @Niladri 我没有给fieldParsers[field] 分配任何东西。我正在阅读它的价值。

标签: typescript


【解决方案1】:

编译器正在保护您免受不太可能发生的事情的影响,您需要决定如何解决它(剧透警告:使用type assertion


想象一下,如果我这样做:

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&lt;F&gt; 的警告。如果F 如上所述是FieldKey,则这种不匹配变得明显。 FieldParsers[F] 将是 FieldParser&lt;'name'&gt; | FieldParser&lt;'price'&gt;,但 FieldParser&lt;F&gt;FieldParser&lt;'name' | 'price'&gt;。前者要么解析string的东西解析number的东西。后者可以解析stringnumber。这些不一样(由于contravariance of function parameters 启用了--strictFunctionTypes)。当上面的代码最终调用 update("name", 9.95) 并且您尝试使用 string 解析器解析 number 时,这些类型之间的差异就会暴露出来。你想要一个FieldParser&lt;F&gt;,但你只有一个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 的值是单个字符串文字值,并且您所做的事情是安全的。

要消除该错误,您可能需要执行type assertion。要么您知道没有人可能通过将 F 扩大到 FieldKey 来故意在脚上开枪,或者您已使用 NotAUnion 之类的东西阻止调用者这样做。无论哪种情况,您都可以告诉编译器您知道fieldParsers[field] 将是一个有效的FieldParser&lt;F&gt;

function update<F extends FieldKey>(field, value: FieldInputs[F]) {
  const parser = fieldParsers[field] as FieldParser<F>; // okay
  parser.apply(value);
}

这样就行了。希望有帮助。祝你好运!

【讨论】:

  • 您是正确的,问题来自编译器将F 想象为联合类型这一事实。在您的第一个示例中F="name"|"price"。 “编译器根本不够聪明,无法理解 F 的值是单个字符串文字值,并且您所做的事情是安全的。”你认为这种聪明有名字吗?我应该提交错误吗?
  • 我不知道...这不是一个错误,只是编译器可以执行的类型分析类型的限制。至于名字,不确定。我想我之前可能已经看到过类似的问题,但我的搜索没有提出任何问题。
【解决方案2】:

我认为您可能对这些类型有点过分了。简化它们实际上会解决您的问题。

让我们从查看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 变量,不如使用一个函数来构建更新函数。

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

【讨论】:

  • 这个type FieldParser&lt;T&gt; = (value?: T) =&gt; Validated&lt;T&gt;; 不适合我。实际上,我的输入/输出值不同。 (例如,输入可以是字符串,输出 - 解析的电话号码)。是的,通过进行这种关键的简化,您将使代码更简单和正确,但这不是我不会做的。我想使用两个接口(输入和输出)对函数实现进行类型检查。
  • 啊,我明白了。所以仅仅因为FieldInputs 接口中的pricenumber,您可能希望该字段的类型在验证后类似于Validated&lt;CurrencyAmount&gt;
  • 是的。我有很多这样的接口对,带有不同的解析器集。我正在编写一些通用代码来处理这些集合(组合、聚合)
猜你喜欢
  • 1970-01-01
  • 2023-01-29
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多