映射类型&泛型函数输入错误

时间:2018-05-30 17:33:08

标签: typescript

我写了一个相当简单的基于映射类型的代码,它不想因某种原因键入check。

首先,定义输入和输出:

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

2 个答案:

答案 0 :(得分:2)

编译器正在保护您免受不太可能发生的事情的影响,您需要决定如何解决它(扰乱警报:使用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<F>。如果上面FFieldKey,则此不匹配变得明显。 FieldParsers[F]将为FieldParser<'name'> | FieldParser<'price'>,但FieldParser<F>FieldParser<'name' | 'price'>。前者要么解析string 解析number的东西。后者可以解析 stringnumber。这些不一样(由于--strictFunctionTypes启用了contravariance of function parameters)。当上面的代码最终调用update("name", 9.95)并尝试使用number解析器解析string时,会暴露这些类型之间的差异。您需要一个FieldParser<F>,但您拥有的只是FieldParsers[F]

现在正在备份,有人可能玩这样的游戏F是价值联盟吗?如果是这样,那么您可能希望更改update()的定义,以明确禁止F成为单个字符串文字。有点像...

type NotAUnion<T> = [T] extends [infer U] ? 
  U extends any ? [T] extends [U] ? T : never : never : never;

declare function update<F extends FieldKey>(
  field: F & NotAUnion<F>, 
  value: FieldInputs[F]
);

但这可能是矫枉过正的,仍然无法解决update()实施中的警告。编译器根本不够聪明,无法理解F的值是单个字符串文字值,而您正在做的事情是安全的。

要使该错误消失,您可能想要type assertion。要么你知道没有人可能通过扩大FFieldKey来故意射击自己,或者你通过使用像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);
}

这样有效。希望有所帮助。祝你好运!

答案 1 :(得分:0)

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

让我们首先看一下FieldParser的类型定义:

type FieldParser<F extends FieldKey> = (value?: FieldInputs[F]) => ParsedFields[F];

它真正做的就是接受一个值并返回相同类型的Validated对象。我们可以将其简化为:

type FieldParser<T> = (value?: T) => Validated<T>;

这不仅提高了复杂性,而且还极大地提高了该类型的可读性。

但请注意,这确实意味着我们已经失去了对FieldParser的限制,即它只能与来自FieldKey的密钥一起使用。但实际上,如果你考虑“Field Parser”的通用概念,它应该是通用的,正如我们将在一秒钟内看到的那样,这并不意味着你的消费代码变得不那么严格了。

我们还可以将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将简单地接受(和typecheck)调用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