Typescript 泛型,keyof 被使用,但它也用符号 | 计算编号 |字符串也是

Typescripts Generics, keyof being used, but TS also compute with symbol | number | string as well

让我们直接进入示例。

在下面的代码中你可以看到 keyof 被使用,但是打字稿仍然计算 EVT 的类型为 string|number|string,即使 EVT 扩展 [=16] =]

代码:(最后一行自动完成和类型检查工作正常)

export const Events = {
    // [event: string]: (...args: any) => void;
    account: {
        available: (status?:  'online'|'offline',foo?:'test',bar?:'test2') => {
            console.error('1');
        },
    }
}

export  class AMQP {
    static event<
        OBJ extends keyof typeof Events,
        EVT extends keyof ((typeof Events)[OBJ]),
        FUNC extends ((typeof Events)[OBJ][EVT])
        >(
        obj: OBJ,
        event: EVT,
        ...args: Parameters<FUNC>
    ) {

    }
}

AMQP.event('account','available','online','test','test2');

Playground Link

问题(悬停在 'FUN' 上的红旗)

Type 'FUNC' does not satisfy the constraint '(...args: any) => any'.
  Type '{ account: { available: (status?: "online" | "offline" | undefined, foo: "test", bar: "test2") => void; }; }[OBJ][EVT]' is not assignable to type '(...args: any) => any'.
    Type '{ account: { available: (status?: "online" | "offline" | undefined, foo: "test", bar: "test2") => void; }; }[OBJ][keyof { account: { available: (status?: "online" | "offline" | undefined, foo: "test", bar: "test2") => void; }; }[OBJ]]' is not assignable to type '(...args: any) => any'.
      Type '{ account: { available: (status?: "online" | "offline" | undefined, foo: "test", bar: "test2") => void; }; }[OBJ][string] | { account: { available: (status?: "online" | "offline" | undefined, foo: "test", bar: "test2") => void; }; }[OBJ][number] | { ...; }[OBJ][symbol]' is not assignable to type '(...args: any) => any'.
        Type '{ account: { available: (status?: "online" | "offline" | undefined, foo: "test", bar: "test2") => void; }; }[OBJ][string]' is not assignable to type '(...args: any) => any'.(2344)

更新:::

截至目前。按照@Maciej Sikora 的建议,通过强制将 FUN 声明为函数修复错误来解决问题。

但这是另一个例子。在没有强制声明的情况下工作正常 唯一的区别是事件不深,所以它在那里工作,但当对象更深一点时,我仍然很想知道原因。

Playground Link

按条件类型缩小类型

我们可以通过明确地告诉 TS 我们只需要函数来修复它,即使目前只有看起来 TS 无法缩小它的范围。考虑:

type E = typeof Events // for readability

export  class AMQP {
    static event<
        OBJ extends keyof E,
        EVT extends keyof E[OBJ],
        FUNC extends E[OBJ][EVT] extends (...a:any[]) => any ?  E[OBJ][EVT] : never
        >(
        obj: OBJ,
        event: EVT,
        ...args: Parameters<FUNC>
    ) {

    }
}

要点是——FUNC extends E[OBJ][EVT] extends (...a:any[]) => any ? E[OBJ][EVT] : never。这意味着我们只允许在对象结构的第二层中选择函数。看来TS已经不抱怨了

这种解决方案的额外好处是,如果我们提供一个不是函数的字段,将无法使用该选择调用该函数。

为什么TS不能缩小类型

有一些原因可以部分解释。考虑类似的例子:

const a = {
    a: 2
}
const f = <A extends typeof a, K extends keyof A>(a: A, k: K) => {
    return a[k] + 1; // error we cannot use +
}
f(a, 'a');

如果a里面只有数字值,我们不能使用+是怎么回事。貌似不对?不正确,因为我们的函数说我们允许 extends typeof a 类型,所以它与 a 不同,我们可以有具有 属性 a 的对象,但也有另一个属性 b 例如布尔值。考虑以下代码编译:

const b = {
    a: 2,
    b: true,
}
f(b, 'b');

所以我可以使用结构上具有相同字段的对象 - a 但也可以使用另一个具有不同类型的对象。出于同样的原因,您的代码无法缩小类型,因为我们可以引入另一个结构相同但具有其他非函数属性的对象。

在我们的示例中,我们的情况略有不同,因为我们指的是一个单独的对象。然后还不完全清楚为什么在某些嵌套级别上推理失败。 TS 需要额外的帮助来缩小功能。为什么对我来说是未知的,看起来像推理限制。

为事件引入静态类型

您应该考虑的第二种解决方案是静态键入事件接口。考虑:

type Events = Record<string, Record<string, (...a: any[]) => any>>
export const events = {
    account: {
        available: (status?: 'online' | 'offline', foo: 'test', bar: 'test2') => {
            console.error('1');
        },
    }
};

export  class AMQP {
    static event<
        INSTANCE extends Events,
        OBJ extends keyof INSTANCE,
        EVT extends keyof INSTANCE[OBJ],
        FUNC extends INSTANCE[OBJ][EVT]
        >(
        inst: INSTANCE,
        obj: OBJ,
        event: EVT,
        ...args: Parameters<FUNC>
    ) {

    }
}
// pay attention below I pass events object explicitly as first argument
AMQP.event(events, 'account', 'available', 'online', 'test', 'test2' );

请注意,我在这里介绍了一些东西:

  • 事件类型
  • 附加的第一个参数是事件接口的实例对象

这样的解决方案更灵活,因为实现不绑定到一个对象,你可以通过Events接口传递任何结构正确的对象,也不需要条件类型。