Typescript-具有绑定泛型类型的函数';不可转让';到扩展类型

Typescript-具有绑定泛型类型的函数';不可转让';到扩展类型,typescript,generics,types,Typescript,Generics,Types,我有一个基本类型base,我想创建一个对象,该对象包含接收base某些子类型的函数(ExtendsBaseA或ExtendsBaseB),并将其映射到另一个类型C 我已尝试将“Base的某些子类型”声明为,但类型检查失败,原因如下: 类型'(baseA:ExtendsBaseA)=>C'不可分配给类型'(base:T)=>C' 参数“base”和“baseA”的类型不兼容 类型“T”不可分配给类型“ExtendsBaseA” 类型“Base”不可分配给类型“ExtendsBaseA” 类型“Ba

我有一个基本类型
base
,我想创建一个对象,该对象包含接收
base
某些子类型的函数(
ExtendsBaseA
ExtendsBaseB
),并将其映射到另一个类型
C

我已尝试将“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;
    }
}

它可以在操场上使用,但当我在本地的Typescript安装上尝试它时就不行了。(两者都是2.9.1)

由于您计划将
处理程序
作为一个映射,其键对应于受歧视的联合的判别式,因此您应该能够准确地表示该类型。让我来充实一下,你必须包括判别式的类型:

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 
}
现在您可以这样定义
Foo
类:

class Foo {
  private readonly handlers: HandlerMap;

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

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

}
TypeScript编译器的控制流分析不够复杂,无法理解
h[b.type]
的参数始终与
b
的类型完全对应。相反,它看到
h[b.type]
接受
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
}
函数句柄(h:HandlerMap,b:b):C |未定义{
常量handler=h[b.type]as((b:b)=>C)|未定义;
如果(!handler)返回;
返回处理程序(b);//好的
}

希望这能有所帮助。祝你好运

我不满意关于拥有一个了解所有扩展类/接口的联合类的建议要求,所以我进一步研究了解决这个问题的方法

值得注意的是,错误是相对合法的——系统无法知道我们所做的总是有效的,所以它告诉我们是这样的。因此,我们基本上必须围绕类型安全展开工作-就像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 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));
注:

  • 添加到全局/占位符中,以减少示例
  • 我喜欢将事件定义作为类,以实现良好的构造函数、易用性、清晰性和安全性。与接口+create函数相反,它让每个人自己编写
    {type:'xxx'}
    或单个变量名。太方便了
  • C在这个最小的例子中是无效的。函数的返回类型是这里实际类型问题的辅助
我偶尔会重温这个答案,以便在学习和扩展系统以及安全化时有所改进:

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

导出接口IDerivedClass{
类型:字符串;
新(…args:any[]):T;
}
导出类基{
//不支持静态抽象-别忘了在派生类中添加它!
//静态类型:字符串;
类型:string=(this.constructor作为IDerivedClass);
Is(ctor:IDerivedClass):这是T{
返回(this.type==ctor.type);
}
}
导出类ExtendsBaseNN扩展基
{
静态类型='NN';
...
}
//显示调度器的易用性
dispatcher.On(ExtendsBaseNN.type,(nn:ExtendsBaseNN)=>{…})
//显示typeguard:
const obj:Base=新扩展的basenn(5,5);
如果(obj.Is(extendsbasernn))返回obj.value1*obj.value2;

在操场上打开
StricFunctionTypes
以查看错误。您打算如何使用
处理程序
?如果每个属性都是只处理
Base
的某些子类型而不是
Base
的所有子类型的函数,则无法使用任何参数调用它(因为您永远不知道您拥有的
Base
对象是否是该函数的“正确”对象)。要么让函数接受
Base
的所有子类型,要么用类型安全的方法将每个
Base
分派给适当的处理函数。如果您能详细说明您计划如何实际使用
处理程序
,我可能会提出一个具体的建议。@jcalz感谢您的建议。因此,我有不同的
Base
子类型,我希望以不同的方式处理它们,并且
Base
上的
type
string属性帮助我唯一地区分它们。我想使用
Base.type
string:
this.handlers[obj.type](obj)
@jcalz调度相应的
handler
函数,我想这些被称为“有区别的联合”,typescript可以使用类型保护来处理它们。然而,在这种情况下,我只想阻止类型系统的阻碍,让我通过索引映射来完成。我想我可以做
[key:string]:(base:any)=>C
,但是
any
觉得太过宽容了,因为我的函数参数总是
base
的子类型。它只是觉得有办法做到这一点谢谢,我花了一些时间去理解
Extract
做了什么,但是得到了答案
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));
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;