如何解决泛型处理程序的strictFunctionTypes错误?

时间:2018-01-23 18:50:40

标签: typescript

我们已经构建了一个管道,用于处理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中工作正常,但我们2.6 strictFunctionTypes mapping的{​​{1}}收到错误:

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

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

2 个答案:

答案 0 :(得分:3)

关于类型断言的其他答案是好的,如果你很乐意告诉编译器不要担心参数的逆转。但是,如果您希望编译器实际上以类型安全的方式帮助您使用这些内容,则需要明确定义mapping的键以及它与{{的确切输入和输出类型的关系。 1}}有问题。我们走了:

首先,让我们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的功能。我只会指定mappingget() ...这不完美,但您可以根据需要进行调整:

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>是相关属性的I属性inputProcessorTypeMappings是相应的O属性。 output类似,只是它既包含键也包含值。

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

ProcessorMap

那些类型检查罚款。我使用const mapping: ProcessorMap = new Map<string, any>(); mapping.set('one', processorOne); // okay mapping.set('two', processorTwo); // okay 而不是使用构造函数参数来做到这一点,这样做会更烦人(因为构造函数是它自己的类型)。

并且,稍后使用set()时,您将获得类型安全性:

mapping

Playground link

希望有所帮助;祝你好运!

更新

我想到了(回想起来这是非常明显的)如果你真的只有一个从字符串值键到特定处理器类型的简单映射,那么你可能想要使用一个普通的旧的常规对象而不是任何类型的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 。字符串值键和不同类型的值是对象最好的。而TypeScript将有助于推断出这种对象的相当具体的类型,因此您甚至不需要声明一堆乱码或接口:

这是您的Map

mapping

就是这样。并测试它:

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

非常简单。你可能会认为你的用例很复杂,需要上面较重的机器;随你(由你决定。但更简单通常更好,所以我想确保提出它。

好的,祝你好运!

答案 1 :(得分:1)

The compiler is not wrong about the fact it can't prove that the assignment processorOne to Processor is safe. You say you "know" based on a type property that you need to pass InputOne to processorOne even though the argument to Process is of type BaseInput.

The simplest solution for when we "know" something the compiler does not know is to use a type assertion:

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

The safer solution would be to make Processor generic and use a custom map that knows about the generic arguments to Processor. But here you are basically pushing the assertions to the map and forging your consumers to specify the type arguments which is not ideal.

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);