如何根据另一个参数中的 属性 的值设置类型?

How to set types based on a value of a property in another parameter?

如何根据方法中另一个参数的 属性 值设置类型?我对 TypeScript 比较陌生。

这是我试过的:

class SomeClass {
    // constructor, other properties, etc
    listen<K extends keyof ClientEvents>({ event: K }: ClientEvent, listener: (...args: ClientEvents[K]) => any)
}

在这里,ClientEvents 看起来像这样:

interface ClientEvents {
    "event-1": [data: number]
    "event-2": [data: number]
    "event-3": [data: any]
}

ClientEvent(没有s)看起来像这样:

interface ClientEvent {
    event: keyof ClientEvents
    anotherProperty: any // I don't want to share all the properties
}

目前,event 字段 自动填充到 event-1,但函数的第一个参数显示为 any 类型,但它应该是 number 类型。如何在不需要更多参数的情况下解决此问题?

ClientEvents 内部的 data 之一更改为类型 string 时,它显示为类型 string | number

class SomeClass {
  listen<K extends keyof ClientEvents>({ event: K }: ClientEvent, listener: (...args: ClientEvents[K]) => any): void;
}

interface ClientEvents {
  "event-1": [data: number]
  "event-2": [data: string]
}

interface ClientEvent {
  event: keyof ClientEvents
  anotherProperty: any
}

new SomeClass().listen({ 
  event: "event-1", 
  anotherProperty: 1 
}, (arg) => { 
  arg // type is string | number
})

这是一个Playground Link

在界面中使用类型参数的解决方法

export interface ClientEvent<L = keyof ClientEvents> {
    event: L,
    site: string
}

export class SomeClass {
    listen<K extends keyof ClientEvents>({ event }: ClientEvent<K>, listener: (...args: ClientEvents[K]) => any): SomeClass
}

和 JavaScript 代码:

let a = new Client()
a.listen({
    event: "event-2"
}, (a) => {
    a
})

a 是一个字符串,所以它工作得很好。

Playground Link

请参阅 the FAQ entry on destructuring assignment and type annotations 以获得可能的权威答案。

您的问题是 { event: K }: ClientEvent 中的 K 不是您认为的那样。在 JavaScript 中,如果你有一个像 { event: K } 这样的函数参数,你正在使用 destructuring assignment with variable renaming,所以你将把那个函数参数的 event 属性 复制到一个变量名为 K:

function foo({event: K}: ClientEvent) {
  // variable -----> ^
  console.log(K.toUpperCase()); 
  //          ^ <---- see?      
}
foo({event: "event-1", anotherProperty: 123})  // EVENT-1

因为 JavaScript 支持这个,所以 TypeScript 也支持(正如你在 ClientEvent type annotation 中看到的),因此调用签名本身只使用 K 作为它忽略的虚拟参数名称。

当然,K 不能用作同名类型参数 K 的推理站点,因为它们不相关。


您试图将 K 用作 类型注释 event 属性。但这不受支持。事实上,目前还没有办法将类型注释放入解构对象本身。在 microsoft/TypeScript#29526 有一个开放的功能请求要求一些方法来做到这一点(也许......双冒号?像 {event::K})但现在这是不可能的。

如果你想给event属性一个类型,你必须在实际的类型注解中,在解构对象之后做:

{ event }: { event: K }

如果您想保留整个事物需要 ClientEvent 的事实,您需要用 intersection type

之类的东西来表达这一点
{ event }: { event: K } & ClientEvent

或者可能通过扩展您的 ClientEvent 界面(如果看起来更好的话):

interface ClientEventFor<K extends keyof ClientEvents> extends ClientEvent {
  event: K
}

declare class SomeClass {
  listen<K extends keyof ClientEvents>(
    { event }: ClientEventFor<K>,
    listener: (...args: ClientEvents[K]) => any): void;
}

然后事情就会开始为你工作:

new SomeClass().listen(
  { event: "event-1", anotherProperty: 1 },
  (arg) => { arg.toFixed() } // okay, arg is number here
)

Playground link to code