我写了一个相当简单的基于映射类型的代码,它不想因某种原因键入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'.
我错过了什么?
答案 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
的类型为FieldKey
而value
的类型为FieldInputs[FieldKey]
,并且有50%的可能性与它们不匹配起来。尽管如此,编译器并没有警告你:它推断F
是FieldKey
(这是完全有效的事情),并且允许调用update()
。
在update()
的实施过程中,警告FieldParsers[F]
可能不是FieldParser<F>
。如果上面F
为FieldKey
,则此不匹配变得明显。 FieldParsers[F]
将为FieldParser<'name'> | FieldParser<'price'>
,但FieldParser<F>
为FieldParser<'name' | 'price'>
。前者要么解析string
或解析number
的东西。后者可以解析 string
或number
。这些不一样(由于--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。要么你知道没有人可能通过扩大F
到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);
}
这样有效。希望有所帮助。祝你好运!
答案 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