如何在打字稿中表达混合 ad-hoc 和参数多态?

How to express mixed ad-hoc and parametric polymorphic in typescript?

我不确定我是否在描述标题中当前的任务。 我想问的是以下要求。

我正在尝试对有限状态机的状态进行抽象,并得出以下定义(打字稿)

interface IState {
    send<T, E>(message: T, callback?:(event: E)=>void): IState;
}

我想表达的是有限状态机的一个状态应该能够接受消息和return新状态,在转换过程中有一个可选的回调来处理事件。

将此接口实现为具体状态时,出现问题。


例如,我正在尝试制作一个只有两个状态的简单状态机,LEFTRIGHT,三个状态可能的消息 go-onturn-leftturn-right。以下table表示它们的关系

重点是我要约束状态LEFT只接受go-onturn-right 消息,而发送 turn-leftLEFT 预计是 编译错误.

我尝试在打字稿 3.4.5 中实现如下内容。

class Left implements IState {
  send(m: 'go-on', cb?: (e: never) => void): Left;
  send(m: 'turn-right', cb?: (e: never) => void): Right;
  send(m: 'go-on' | 'turn-right', cb?: any) {
    return m === 'go-on' ? new Left() : new Right();
  }
}

class Right implements IState {
  send(m: 'go-on', cb?: (e: never) => void): Right;
  send(m: 'turn-left', cb?: (e: never) => void): Left;
  send(m: 'go-on' | 'turn-left', cb?: any) {
    return m === 'go-on' ? new Right() : new Left();
  }
}

实现没有编译错误,auto-complete按预期工作。但因为它看起来很奇怪,所以我提出了一个问题 TypeScript function generic can only work for function overload with more than one signatures

感谢您在该问题下的友好回复,我知道将重载函数分配给泛型函数是错误的。但是,如何在保持特定状态只接受所需类型的消息的同时表达状态的通用接口?

related GitHub issue


我能想到的另一个抽象是

interface IState<T, E, R extends IState<?, ?, ?>> {
    send(message: T, callback?:(event: E)=>void): R;
}

但是return类型是递归的,上面那三个问题不知道填什么。

更简单的版本可以是

interface IState<T, E> {
    send(message: T, callback?:(event: E)=>void): IState<any, any>;
}

除了 return 类型中烦人的 any 之外,它似乎表现得像。

interface IState {
    send<T, E>(message: T, callback?:(event: E)=>void): IState;
}

我找到了 maybe related issue in GitHub about generic value


这个问题是well-defined吗?

如果为真,上面列出的这些方法是否有正确的解决方案?

如果为假,正确的解决方案是什么?

我认为最好的选择是这个interface IState<T, E, R extends IState<?, ?, ?>>。问号可以用 any 代替,我们真的不在乎之后的状态是什么,只是它是某种状态。

interface IState<T, E, R extends IState<any, any, any>> {
    send(message: T, callback?: (event: E) => void): R;
}

class Left implements IState<'go-on', never, Left>, IState<'turn-right', never, Right>{
    send(m: 'go-on', cb?: (e: never) => void): Left;
    send(m: 'turn-right', cb?: (e: never) => void): Right;
    send(m: 'go-on' | 'turn-right', cb?: any) {
        return m === 'go-on' ? new Left() : new Right();
    }
}

class Right implements IState<'go-on', never, Right>, IState<'turn-left', never, Left> {
    send(m: 'go-on', cb?: (e: never) => void): Right;
    send(m: 'turn-left', cb?: (e: never) => void): Left;
    send(m: 'go-on' | 'turn-left', cb?: any) {
        return m === 'go-on' ? new Right() : new Left();
    }
}

let left = new Left();
let left_go_on: Left = left.send("go-on")
let left_turn_right: Right = left.send("turn-right")
left.send("turn-left") // error


let right = new Right();
let right_go_on: Right = right.send("go-on")
let right_turn_right: Left = right.send("turn-left")
right.send("turn-right") // error

或者,如果您只想在 implements 子句中使用接口,这也适用:

interface IState<T extends [any, any, IState<[any, any, any]>]> {
    send: T extends T  ? ((message: T[0], callback?: (event: T[1]) => void) => T[2]) : never
}

class Left implements IState<['go-on', never, Left] | ['turn-right', never, Right]>{
    send(m: 'go-on', cb?: (e: never) => void): Left;
    send(m: 'turn-right', cb?: (e: never) => void): Right;
    send(m: 'go-on' | 'turn-right', cb?: any) {
        return m === 'go-on' ? new Left() : new Right();
    }
}

class Right implements IState<['go-on', never, Right] | ['turn-left', never, Left]> {
    send(m: 'go-on', cb?: (e: never) => void): Right;
    send(m: 'turn-left', cb?: (e: never) => void): Left;
    send(m: 'go-on' | 'turn-left', cb?: any) {
        return m === 'go-on' ? new Right() : new Left();
    }
}

let left = new Left();
let left_go_on: Left = left.send("go-on")
let left_turn_right: Right = left.send("turn-right")
left.send("turn-left") // error


let right = new Right();
let right_go_on: Right = right.send("go-on")
let right_turn_right: Left = right.send("turn-left")
right.send("turn-right") // error