在TypeScript中根据泛型ResultType进行翻译。

huangapple go评论59阅读模式
英文:

Depending Generic ResultType in Typescript

问题

Hi everyone!

interface Data {
  date: Date;
  message?: string;
}

interface DataB {
  dateB: Date;
  messageB?: string;
}

class DataResolver {
  public submit(): Data {
    return { date: new Date() };
  }

  public submitB(): DataB {
    return { dateB: new Date() };
  }
}

interface StringKeyedObject {
  [key: string]: any;
}

class Handler<C extends StringKeyedObject> {
  send<
    K extends Extract<keyof C, string>,
    L extends ReturnType<Extract<keyof K, object>>,
  >(eventName: K, arg: L) {
    //
  }
}
const handler = new Handler<DataResolver>();
handler.send('submit', null);

我真的希望根据第一个value参数设置第二个arg参数。在VS Code的自动完成中,建议使用submitsubmitB,这正是我想要的。如果我选择submit,我希望arg的类型为Data。如果我选择submitB,类型应该为DataB

我的尝试使用泛型类型L extends ReturnType<Extract<keyof K, object>> 完全不起作用。显然。

希望你能找到一个解决方案!谢谢,一切顺利! 😊

英文:

😊 Hi everyone!

interface Data {
  date: Date;
  message?: string;
}

interface DataB {
  dateB: Date;
  messageB?: string;
}

class DataResolver {
  public submit(): Data {
    return { date: new Date() };
  }

  public submitB(): DataB {
    return { dateB: new Date() };
  }
}

interface StringKeyedObject {
  [key: string]: any;
}

class Handler&lt;C extends StringKeyedObject&gt; {
  send&lt;
    K extends Extract&lt;keyof C, string&gt;,
    L extends ReturnType&lt;Extract&lt;keyof K, object&gt;&gt;,
  &gt;(eventName: K, arg: L) {
    //
  }
}
const handler = new Handler&lt;DataResolver&gt;();
handler.send(&#39;submit&#39;, null);

I really would like to set the second arg parameter depending on the first value paramter. In VS Code autocomplete suggests submit and submitB as intended. 😊 Now if I select submit I would like to be arg of type Data. If I select submitB the type should be DataB.

My attempt with generic type L extends ReturnType&lt;Extract&lt;keyof K, object&gt;&gt; does not work at all. Obviously.

I hope you'll find a solution! Thanks an all the best! 😊

答案1

得分: 1

这段代码中,Handler 类接受一个泛型参数 C,其类型必须为拥有字符串键的对象。send 方法接受两个参数,value 是字符串类型的键,arg 的类型依赖于 C[K] 的类型。如果 C[K] 是函数类型,则 arg 的类型为该函数的返回值类型;如果 C[K] 不是函数类型,则 arg 的类型为 never。这确保了 arg 参数的类型与输入的键对应的属性类型一致。

英文:

I would write it like this:

class Handler&lt;C extends StringKeyedObject&gt; {
  send&lt;K extends Extract&lt;keyof C, string&gt;&gt;(
    value: K, arg: C[K] extends (...args: any) =&gt; infer R ? R : never) {
    // do stuff
  }
}

Here the arg parameter type depends on K. It is essentially the same as ReturnType&lt;C[K]&gt;, so let's first look at that version first:

class Handler&lt;C extends StringKeyedObject&gt; {
  send&lt;K extends Extract&lt;keyof C, string&gt;&gt;(
    value: K, arg: ReturnType&lt;C[K]&gt; ) {
    // do stuff
  }
}

The type C[K] is an indexed access type meaning "the property type you'd read when indexing into an object of type C with a key of type K". It is presumably what you were trying to do with keyof K, except that the keyof operator on K would give you "the keys of the keys of C", which is like, whatever keys a string has... toUppercase and such. Not what you wanted.

And note that, unless you have a special reason, you don't need to have a separate type parameter for arg. You could have written L extends ReturnType&lt;C[K]&gt; and used L, but we don't need some arbitrary subtype of ReturnType&lt;C[K]&gt; for this to work, so there's no reason to add another type parameter.


So, given this definition, let's make sure it works:

class DataResolver {
  public submit(): Data { return { date: new Date() }; }
  public submitB(): DataB { return { dateB: new Date() }; }
  a = 2 // I added this
}
const handler = new Handler&lt;DataResolver&gt;();
handler.send(&quot;submit&quot;, { date: new Date() }); // okay
handler.send(&quot;submitB&quot;, { dateB: new Date() }); // okay
handler.send(&quot;a&quot;, &quot;wha&quot;) // &lt;-- shouldn&#39;t compile but it does

Everything works the way you want, except... if your DataResolver happens to have a non-function property, we don't want the compiler to accept any call to handler.send() for that property. One way to do that is to check whether the property value is a function type, and if so, use its return type; and if not, use the impossible never type. That brings us back to:

class Handler&lt;C extends StringKeyedObject&gt; {
  send&lt;K extends Extract&lt;keyof C, string&gt;&gt;(
    value: K, arg: C[K] extends (...args: any) =&gt; infer R ? R : never) {
    // do stuff
  }
}

where arg is a conditional type that depends on C[K]. The use of infer lets us extract out the return type as its own type parameter R and use it.

By the way, this is very similar to how ReturnType&lt;T&gt; is defined:

type ReturnType&lt;T extends (...args: any) =&gt; any&gt; = 
  T extends (...args: any) =&gt; infer R ? R : any;

except that this version gives the any type when T isn't a function, and since any accepts anything, the call to handler.send(&quot;a&quot;, &quot;wha&quot;) succeeds when it shouldn't.

Okay, let's test it one more time:

const handler = new Handler&lt;DataResolver&gt;();
handler.send(&quot;submit&quot;, { date: new Date() }); // okay
handler.send(&quot;submitB&quot;, { dateB: new Date() }); // okay
handler.send(&quot;a&quot;, &quot;wha&quot;) // error!
// -------------&gt; ~~~~~
// Argument of type &#39;string&#39; is not assignable to parameter of type &#39;never&#39;.

Looks good. There are other ways to harden send against bad inputs (we could make it so that you're not even allowed to give it keys corresponding to non-functions) but I don't want to digress even further from the question as asked.

Playground link to code

huangapple
  • 本文由 发表于 2023年3月31日 04:02:00
  • 转载请务必保留本文链接:https://go.coder-hub.com/75892523.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定