如何解决泛型处理程序的严格函数类型错误?



我们在TypeScript中构建了一个管道来处理MongoDB集合中的数据。

集合中的每个文档都有一个我们用来区分的type属性。然后,我们通过处理器发送每个文档。

下面是我们代码的人为/简化版本:

type Processor = (input: BaseInput) => BaseOutput;
// base
interface BaseInput {
name: string;
}
interface BaseOutput {
id: number;
}
// one
interface InputOne extends BaseInput {
title: string;
}
interface OutputOne extends BaseOutput {
fullname: string;
}
// two
interface InputTwo extends BaseInput {
age: number;
}
interface OutputTwo extends BaseOutput {
parent: string;
}
const mapping = new Map<string, Processor>([
['one', processorOne],
['two', processorTwo],
]);
function processorOne(input: InputOne): OutputOne {
return {
id: 1,
fullname: input.title + input.name,
}
}
function processorTwo(input: InputTwo): OutputTwo {
return {
id: 2,
parent: input.name,
}
}

这在2.5中工作正常,但我们在mappingstrictFunctionTypes2.6中出现错误:

Argument of type '([string, (input: InputOne) => OutputOne] | [string, (input: InputTwo) => OutputTwo])[]' is not assignable to parameter of type '[string, Processor][]'.
Type '[string, (input: InputOne) => OutputOne] | [string, (input: InputTwo) => OutputTwo]' is not assignable to type '[string, Processor]'.
Type '[string, (input: InputOne) => OutputOne]' is not assignable to type '[string, Processor]'.
Type '(input: InputOne) => OutputOne' is not assignable to type 'Processor'.
Types of parameters 'input' and 'input' are incompatible.
Type 'BaseInput' is not assignable to type 'InputOne'.
Property 'title' is missing in type 'BaseInput'.

我理解为什么它会警告我,但是我们如何调整声明(或实现)以使其类型安全?

关于类型断言的其他答案很好,如果你愿意告诉编译器不要担心参数逆变。 但是,如果您希望编译器实际帮助您以类型安全的方式使用这些内容,则需要显式定义mapping的键以及它与相关Processor的确切输入和输出类型的关系。 来吧:

首先,让我们Processor通用,以便您可以准确表达您正在使用的Processor类型:

type Processor<I extends BaseInput=BaseInput, O extends BaseOutput=BaseOutput>
= (input: I) => O;

因此,普通Processor与以前相同,但现在您可以专门讨论Processor<InputOne, OutputOne>作为处理器,它将InputOne作为输入并产生和OutputOne作为输出。

现在让我们描述一下您要存储在mapping中的事物的类型:

interface ProcessorTypeMappings {
one: { input: InputOne, output: OutputOne };
two: { input: InputTwo, output: OutputTwo };
}

这有点冗长(稍后会导致更冗长),但如果您需要查询,例如,仅查询mapping.get('two')的输入类型,稍后会有所帮助。 在任何情况下,您都可以为更多处理器添加上述属性。

我们可以使用ProcessorTypeMappings更准确地描述mapping的功能。 我只打算指定get()set()...这并不完美,但您可以根据需要进行调整:

interface ProcessorMap extends Map<string, Processor<any, any>> {
get<K extends keyof ProcessorTypeMappings>(key: K):
Processor<ProcessorTypeMappings[K]['input'], ProcessorTypeMappings[K]['output']>;
set<K extends keyof ProcessorTypeMappings>(key: K,
val: Processor<ProcessorTypeMappings[K]['input'], ProcessorTypeMappings[K]['output']>): this;
}

所以get()ProcessorTypeMappings中获取一个键,并产生一个值,这是一个Processor<I,O>,其中IProcessorTypeMappings中相关属性的input属性,O是对应的output属性。set()是相似的,只是它同时需要一个键和一个值。

现在,最后,您可以将mapping创建为ProcessorMap并填充它:

const mapping: ProcessorMap = new Map<string, any>();
mapping.set('one', processorOne);  // okay
mapping.set('two', processorTwo);  // okay

那些类型检查很好。 我使用set()而不是使用构造函数参数来做到这一点,这样做会更烦人(因为构造函数是它自己的类型)。

而且,当您稍后使用mapping时,您将获得类型安全:

declare const inputOne: InputOne;
declare const inputTwo: InputTwo;
const outputOne = mapping.get('one')(inputOne); // OutputOne
const oops = mapping.get('three'); // no such key
const nope = mapping.get('two')(inputOne); // InputOne not assignable to InputTwo

游乐场链接

希望有帮助;祝你好运!


更新

我突然想到(回想起来这很明显)如果你真的只有从字符串值键到特定处理器类型的简单映射,那么你可能想使用一个普通的旧常规对象而不是任何类型的Map。 字符串值键和不同类型的值是对象最擅长的。 TypeScript 将帮助推断出这样一个对象的相当特定的类型,所以你甚至不需要声明一堆接口或类型:

这是您的mapping

const mapping = {
one: processorOne,
two: processorTwo
}; 

就是这样。 并对其进行测试:

declare const inputOne: InputOne;
declare const inputTwo: InputTwo;
const outputOne = mapping.one(inputOne); // OutputOne
const oops = mapping.three; // no such key
const nope = mapping.two(inputOne); // InputOne not assignable to InputTwo

简单得离谱。 您可能会认为您的用例足够复杂,需要上述更重的机器;这取决于你。 但越简单越好,所以我想确保呈现它。

好的,再次祝你好运!

编译器没有错,因为它无法证明processorOne分配给Processor是安全的。你说你"知道"基于你需要传递给InputOne的类型属性processorOne即使Process的参数是类型BaseInput

当我们"知道"编译器不知道的东西时,最简单的解决方案是使用类型断言:

const mapping = new Map<string, Processor>([
['one', <Processor>processorOne],
['two', <Processor>processorTwo],
]);

更安全的解决方案是使Processor通用并使用知道要Processor的通用参数的自定义映射。但是在这里,您基本上是将断言推送到映射中,并伪造您的使用者来指定不理想的类型参数。

class ProcessorMap {
private innerMap = new Map<string, any>() ;
set<TIn extends BaseInput, TOut extends BaseOutput>(key: string, processor: Processor<TIn, TOut>) {
this.innerMap.set(key, <any>processor);
return this;
}
get<TIn extends BaseInput, TOut extends BaseOutput>(key: string) {
return this.innerMap.get(key);
}
}
const mapping = new ProcessorMap()
.set('one', processorOne)
.set('two', processorTwo);