打字稿 - 具有绑定泛型类型的函数'不可分配'扩展类型

时间:2018-06-11 14:37:35

标签: typescript generics types

我有一个基类型Base,我想创建一个对象,该对象包含接收Base(在这种情况下为ExtendsBaseAExtendsBaseB)的子类型的函数,并且将其映射到另一种类型C

我已尝试将&#34; Base&#34; 的某些子类型声明为<T extends Base>,但类型检查失败,并显示以下内容:

  

Type&#39;(baseA:ExtendsBaseA)=&gt; C&#39;不能分配类型&#39;(base:T)=&gt; C&#39;

     

参数类型&#39; base&#39;和&#39; baseA&#39;是不相容的。

     

键入&#39; T&#39;不能分配给&#39; ExtendsBaseA&#39;。

     

键入&#39; Base&#39;不能分配给&#39; ExtendsBaseA&#39;。

     

物业&#39; b&#39;类型&#39; Base&#39;

中缺少

interface Base {
    a: string
}

interface ExtendsBaseA extends Base {
    b: string
}

interface ExtendsBaseB extends Base {
    c: string
}

interface C {}


class Foo {
    private readonly handlers: {
        [key: string]: <T extends Base> (base: T) => C
    }

    constructor() {
        this.handlers = {
            'bar' : this.handler
        }
    }

    handler(baseA: ExtendsBaseA): C {
        return <C>null;
    }
}

Try it here

关于如何解决这个问题的任何想法?

修改:如果我改变了,那就太奇怪了:

[key: string]: <T extends Base> (base: T) => C

为:

[key: string]: (base: Base) => C

它在操场上工作,但在我本地的Typescript安装上尝试时却没有。 (均为2.9.1)

2 个答案:

答案 0 :(得分:2)

由于您计划将handlers作为一个映射,其键对应于被区分联合的判别式,因此您应该能够准确地表示该类型。让我充实你必须包括判别式的类型:

interface Base {
  type: string
  a: string
}

interface ExtendsBaseA extends Base {
  type: "ExtendsBaseA"
  b: string
}

interface ExtendsBaseB extends Base {
  type: "ExtendsBaseB"
  c: string
}

interface C { }

type BaseUnion = ExtendsBaseA | ExtendsBaseB;

请注意,您需要明确声明联合,如上面的BaseUnion所示。现在我们可以按如下方式定义类型HandlerMap

type HandlerMap = { 
  [K in BaseUnion['type']]?: (base: Extract<BaseUnion, { type: K }>) => C 
}

如果你检查一下,它看起来像是:

type HandlerMap = {
  ExtendsBaseA?: (base: ExtendsBaseA) => C,
  ExtendsBaseB?: (base: ExtendsBaseB) => C
}

现在您可以像这样定义Foo类:

class Foo {
  private readonly handlers: HandlerMap;

  constructor() {
    this.handlers = {
      ExtendsBaseA: this.handler // changed the key
    }
  }

  handler(baseA: ExtendsBaseA): C {
    return <C>null!;
  }

}

就这一切而言,一切正常。尽管如此,你会发现编写一个类型安全函数会感到很沮丧,该函数需要HandlerMapBaseUnion并尝试生成C

function handle<B extends BaseUnion>(h: HandlerMap, b: B): C | undefined {
  const handler = h[b.type] 
  if (!handler) return;
  return handler(b); // error, no valid call signature
}

TypeScript编译器的控制流分析不够复杂,无法理解h[b.type]的参数将始终与b的类型完全对应。相反,它会发现h[b.type]接受 {em} 成分BaseUnion,而b BaseUnion成员function handle<B extends BaseUnion>(h: HandlerMap, b: B): C | undefined { const handler = h[b.type] as ((b: B) => C) | undefined; if (!handler) return; return handler(b); // okay } 并且他们认为他们不匹配的可能性。你可以声称他们确实匹配,这可能是你能做的最好的事情:

US Dollar

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

答案 1 :(得分:2)

我对拥有一个知道所有扩展类/接口的联合类的建议要求不满意,因此我进一步研究了解决该问题的方法。

值得注意的是,该错误是相对合法的-系​​统无法知道我们在做什么总是有效的,因此它告诉我们了。因此,我们基本上必须解决类型安全问题-jcalz使用as ((b: B) => C)

我从另一个方向着手-告诉编译器内部我知道我在注册时使用类型安全,而不是在回调时在做什么,但我仍然想正确键入从基类派生的处理程序。

所以我们开始:

注册/调度程序:

interface Base {
    type: string;
}

export type Handler<T extends Base> = (action: T) => void;
type InternalHandler = (action: Base) => void;

export class Dispatcher {
    private handlers: { [key: string]: InternalHandler } = {};

    On<T extends Base>(type: string, extHandler: Handler<T>) {
        const handler = extHandler as InternalHandler;
        this.handlers[type] = handler;
    }

    Dispatch(e: Base) {
        const handler = this.handlers[e.type];
        if(handler) handler(e);
    }
}

事件:

export class ExtendsBaseFn implements Base {
    type: 'Fn' = 'Fn';
    constructor(public cb: () => void) { };
}

export class ExtendsBaseNN implements Base {
    type: 'NN' = 'NN';
    constructor(
        public value1: number,
        public value2: number,
    ) { }
}

处理程序:

export class ObjWithHandlers {
    constructor() {
        global.dispatcher.On('Fn', (baseFn: ExtendsBaseFn) => {
            baseFn.cb();
        });
        global.dispatcher.On('NN', (baseNN: ExtendsBaseNN) => {
            console.log(baseNN.value1 * baseNN.value2);
        });
    }
}

驾驶员:

(global as any).dispatcher = new Dispatcher();
const placeholder = new ObjWithHandlers();

最后是用户:

global.dispatcher.Dispatch(new ExtendsBaseFn(() => console.log('woo')));
global.dispatcher.Dispatch(new ExtendsBaseNN(5, 5));

注释:

  • 添加到全局/占位符只是为了最小的示例目的
  • 我喜欢将事件定义作为漂亮的构造函数,易用性,清晰度和安全性的类。与接口+创建函数相反,它使每个人都可以自己写{ type: 'xxx' }或各个变量名。太方便了!
  • 在这个最小示例中,
  • C是无效的。函数的返回类型是此处实际键入问题的辅助。

通过静态访问器获得一些乐趣,甚至可以更安全地输入

export interface IDerivedClass<T> {
    type: string;
}

export class Base {
    static type: string;

    static Type<T>(ext: IDerivedClass<T>): string {
        return ext.type;
    }
    static TypeOf(inst: Base): string {
        const ctor = inst.constructor;
        return Base.Type(ctor);
    }

    type: string;

    "constructor": typeof Base;
    constructor() {
        this.type = Base.TypeOf(this);
    }
}

export class ExtendsBaseNN extends Base
{
    static type = 'NN';
    ...
}

dispatcher.On(ExtendsBaseNN.type, () => {})