简体   繁体   English

Typescript 动态 class 功能来自 generics

[英]Typescript dynamic class functions from generics

I want to do something like我想做类似的事情

class C<R extends Record<string, unknown>> {
  [K extends keyof R](param: R[K]) {
    console.log(K, param); // have access to both key and value
  }
}

Is there a way to achieve this with typescript?有没有办法用 typescript 实现这个?

Currently I have something like目前我有类似的东西

class C<R extends Record<string, unknown>> {
  f<K extends keyof R>(k: K, param: R[K]) {
    console.log(k, param);
  }
}

and I want to see if I can get rid of the f since it's redundant/semantically not useful in my use case.我想看看我是否可以摆脱f因为它在我的用例中是多余的/在语义上没有用。

I read this post, but it only generates the names and not the implementations of the functions.我读了这篇文章,但它只生成名称而不是函数的实现。

I'm going to call your "current" class C<R> :我会打电话给你的“当前” class C<R>

class C<R extends Record<string, unknown>> {
  f<K extends keyof R>(k: K, param: R[K]) {
    console.log(k, param);
  }
  z = 1; // example of some other property of C
}

and we can plan to define a new class D<R> in terms of it that behaves how you want.我们可以计划根据它来定义一个新的 class D<R>来表现你想要的。 The idea will be that D in some sense extends C ... so that every property of C is also a property of D (I've added an extra property for that reason), but D<R> will also have all the properties from R and will dispatch them to the f method of C .这个想法是D在某种意义上扩展C ... 这样C的每个属性也是D的一个属性(为此我添加了一个额外的属性),但D<R>也将具有所有属性从R并将它们分派到Cf方法。 That is, new D<{a: string}>().a("hello") should act the same as new C<{a: string}>().f("a", "hello") .也就是说, new D<{a: string}>().a("hello")应该和new C<{a: string}>().f("a", "hello")一样。


TypeScript will only allow you to declare class es or interface s with "statically known" keys. TypeScript 将只允许您使用“静态已知”密钥声明class es 或interface s。 Since R is a generic type parameter, then keyof R is not "statically known" so the compiler won't let you just use a class statement directly for this purpose.由于R是泛型类型参数,因此keyof R不是“静态已知”的,因此编译器不会让您直接为此目的使用class语句。 Instead, you can describe the type of your D<R> class and then use something like a type assertion to claim that some constructor has the right type.相反,您可以描述D<R> class 的类型,然后使用类型断言之类的东西来声明某些构造函数具有正确的类型。

It will look like它看起来像

type D<R extends Record<string, unknown>> =
  C<R> & { [K in keyof R]: (param: R[K]) => void };

const D = ... as new <R extends Record<string, unknown>>() => D<R>;

where the stuff in ... is the implementation. ...中的内容是实现。 So this will work, but it's somewhat ugly (since it forces you to write out the type explicitly) and error prone (since the compiler can't verify type safety here).所以这会起作用,但它有点难看(因为它迫使你显式写出类型)并且容易出错(因为编译器不能在这里验证类型安全)。


Next, you can't implement a class directly that will work this way.接下来,您不能直接实现将以这种方式工作的class If you have an instance i of your desired class D<R> , and you call ik(x) where "k" is in keyof T , you want the instance to dispatch the call to something like f("k", x) .如果你有一个你想要的 class D<R>的实例i ,并且你调用ik(x)其中"k"keyof T中,你希望实例将调用分派给类似f("k", x) . Unless you have a list of all possible keys of R at runtime somewhere, you can't make real methods for all of them on the prototype of D .除非您在某个运行时拥有R的所有可能键的列表,否则您无法在D的原型上为所有这些键创建真正的方法。 That implies the methods of D need to have dynamic names, and that's not something a class normally does.这意味着D的方法需要具有动态名称,而class通常不会这样做。

If you want dynamic property names in JavaScript, you will want to use a Proxy object which lets you write a get() handler to intercept all property gets.如果您想要 JavaScript 中的动态属性名称,您将需要使用Proxy object ,它允许您编写一个get()处理程序来拦截所有属性获取。 If you make the D class constructor return a Proxy then a call to new D() will produce such a proxy (normally you don't return anything from class constructors, but if you do then new will give you the returned thing instead of this ).如果你让D class 构造函数返回一个Proxy然后调用new D()将产生这样一个代理(通常你不会从 class 构造函数返回任何东西,但如果你这样做那么new会给你返回的东西而不是this ).

So here's one way to do it:所以这是一种方法:

const D = class D {
  constructor() {
    return new Proxy(new C(), {
      get(t, k, r) {
        return ((typeof k === "string") && !(k in t)) ?
          (param: any) => t.f(k, param)
          : Reflect.get(t, k, r);
      }
    });
  }
} as new <R extends Record<string, unknown>>() => D<R>

If k is a key already present in the C instance, then you'll get the property at that key (hence the Reflect.get() call).如果kC实例中已经存在的键,那么您将获得该键的属性(因此调用Reflect.get() )。 Otherwise, you get a callback function which dispatches its parameter to the f() method of the C instance.否则,您会得到一个回调 function,它将其参数分派给C实例的f()方法。

Note that this only works if you can correctly identify which properties should get dispatched to f() at runtime.请注意,这仅在您可以正确识别应在运行时将哪些属性分派给f()时才有效。 In this example, we are sending everything which isn't already a property of C .在此示例中,我们发送的所有内容都不是C的属性。 If you have a need for some more complicated dispatching logic, you will need to write that logic yourself somewhere, probably in the form of a bunch of hard-coded property keys.如果您需要一些更复杂的调度逻辑,您将需要自己在某个地方编写该逻辑,可能采用一堆硬编码属性键的形式。 But that's out of scope for the question as asked.但是对于所问的问题,这是 scope 之外的。

So that might work, but again... it's unusual ( Proxy and a return in a constructor is not best practice) and error-prone ( did I implement it right? ).所以这可能有效,但同样......这是不寻常的( Proxy和构造函数中的return不是最佳实践)并且容易出错(我是否正确实施了它?)。


Let's see if it works:让我们看看它是否有效:

const d = new D<{ a: string, b: number, c: boolean }>();

d.a("hello") // "a", "hello"
d.b(123); // "b", 123
console.log(d.z.toFixed(2)) // 1.00

Yay, it worked.耶,它奏效了。 But I'm not sure it's worth the complexity and fragility of the code just to save a caller a few keystrokes.但我不确定代码的复杂性和脆弱性是否值得仅仅为了让调用者少敲几下键盘。 Personally I'd rather keep the code simple and clean and make people write c.f("a", "hello") instead of da("hello") .就个人而言,我宁愿保持代码简单和干净,让人们写c.f("a", "hello")而不是da("hello") But it's up to you.但这取决于你。


Playground link to code 游乐场代码链接

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM