【问题标题】:Typescript - Function with bound generic type 'not assignable' to extended typeTypescript - 将泛型类型“不可分配”绑定到扩展类型的函数
【发布时间】:2018-11-20 20:30:00
【问题描述】:

我有一个基本类型Base,我想创建一个对象,该对象包含接收Base 的某些子类型(在本例中为ExtendsBaseAExtendsBaseB)的函数并将其映射到另一种类型C .

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

类型 '(baseA: ExtendsBaseA) => C' 不能分配给类型 '(base: T) => C'。

参数'base'和'baseA'的类型不兼容。

类型“T”不能分配给类型“ExtendsBaseA”。

类型“Base”不可分配给类型“ExtendsBaseA”。

“Base”类型中缺少属性“b”。

片段

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)

【问题讨论】:

  • 在 Playground 中打开 strictFunctionTypes 以查看错误。
  • 您打算如何使用handlers?如果每个属性都是一个只处理Basesome 子类型而不是Baseall 子类型的函数,那么就无法使用任何参数调用它(因为您永远不知道您拥有的 Base 对象是否是该函数的“正确”对象)。要么让你的函数接受Base所有 子类型,要么采用一种类型安全的方式将每个Base 分派给适当的处理函数。如果你能详细说明你打算如何实际使用handlers,我可能会提出一个具体的建议。
  • @jcalz 感谢游乐场提示。所以我有不同的Base 子类型,我想以不同的方式处理,Base 上的type 字符串属性可以帮助我唯一区分它们。我想使用Base.type 字符串调度适当的handler 函数:this.handlers[obj.type](obj)
  • @jcalz 我猜这些被称为 "discriminated unions" 并且打字稿可以使用类型保护来处理它们。但是在这种情况下,我只想阻止类型系统妨碍我,让我通过索引地图来做到这一点。我想我可以做到[key: string]: (base: any) =&gt; C,但any 感觉太宽松了,因为我的函数参数总是Base 的子类型。只是觉得会有办法做到这一点

标签: typescript generics types


【解决方案1】:

由于您计划将 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!;
  }

}

就目前而言,这一切都有效。尽管如此,你会发现编写一个类型安全的函数是令人沮丧的,它接受一个HandlerMap 和一个BaseUnion 并试图产生一个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] 接受BaseUnionsome 组成部分,而bBaseUnionsome 组成部分,并拒绝接受它们的可能性不匹配。您可以断言它们确实匹配,这可能是您能做的最好的:

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
}

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

【讨论】:

  • 谢谢,我花了一段时间才明白Extract 做了什么,但最后还是到了那里:)
【解决方案2】:

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

值得注意的是,这个错误是相对合理的——系统无法知道我们所做的事情总是有效的,所以它告诉我们。所以我们基本上必须解决类型安全问题——就像 jcalz 对 as ((b: B) =&gt; 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 在这个最小示例中是无效的。函数的返回类型是此处实际输入问题的辅助。

我偶尔会重新审视这个答案,以便在我学习和扩展系统并确保安全时进行改进:

  • 静态成员类型(v3 - 超级简化)
  • 对子类的类型保护检查(没有父/子依赖项)

export interface IDerivedClass<T extends Base> {
    type: string;
    new (...args: any[]): T;
}

export class Base {
    // no support for static abstract - don't forget to add this in the derived class!
    // static type: string;

    type: string = (this.constructor as IDerivedClass<this>).type;

    Is<T extends IDerivedClass>(ctor: IDerivedClass<T>): this is T {
        return (this.type === ctor.type);
    }
}

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

// show dispatcher ease of use
dispatcher.On(ExtendsBaseNN.type, (nn: ExtendsBaseNN) => {...})

// show typeguard:
const obj: Base = new ExtendsBaseNN(5,5);
if (obj.Is(ExtendsBaseNN)) return obj.value1 * obj.value2;

【讨论】:

  • 不错的方法。另外,您的意思是Handler 而不是ActionHandler
  • 谢谢,并确认+编辑。 (我的本地代码是 Action Everything)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-09-19
  • 2020-06-18
  • 1970-01-01
  • 2020-09-12
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多