TypeScript class 中第一个定义的方法使用类型为 this 的函数未正确类型化(但之后的所有内容)

First defined method in TypeScript class is not properly typed (but everything after it is) using functions with typed this

我真的不知道这里可能出了什么问题。本质上,我正在尝试制作一个 MyModel class 来接收一些数据并初始化它自己的值,并定义一些对所述数据起作用的方法。但是,该模型可能只接收数据的特定部分——因此我将 MyModel<Included extends keyof MyData> 定义为 data: Pick<MyData, Included>。我现在只希望在 MyData 的这个子集上运行的方法可以从某些模型实例中调用,因此成员方法是通过 DependentMethod([ ...dependencies], method) 辅助函数定义的,returns 一个需要适当的函数this —— 即 MyModel 在其 Included 中具有某些属性(这也添加了一些运行时保护,因此它 returns undefined 如果它被不正确的调用this 无论如何)。

然后我定义 usernameCapitalised = DependentMethod([ 'username' ], ...) 等,工作正常...除了在 class 中,无论第一个通过 DependentMethod 定义的成员函数的类型是 any 并且根本不进行类型检查。如果我在它之前添加一个新的这样的函数,那个函数不会进行类型检查,但前一个函数会开始这样做。如果我将第一个函数移动到不再是第一个,它会再次开始类型检查,但现在是第一个的函数不会。最初我认为问题出在我使用更高种类的类型上,但正如您在我对最小复制的最佳尝试中看到的那样,情况似乎并非如此:

type UnionOfList<List extends any[]> = List[number]

export const DependentMethod = function<Dependencies extends (keyof MyData)[], R>
  (dependencies: Dependencies, fn: (this: MyModel<UnionOfList<Dependencies>>) => R):
    (this: MyModel<UnionOfList<Dependencies>>) => R
{
  if(dependencies.every(dependency => this.data.includes(dependency))) {
    return fn.call(this)
  } else {
    return undefined
  }
}

let noop = DependentMethod(['username'], () => 0)
class MyModel<Included extends keyof MyData> {
  data: Pick<MyData, Included>
  constructor(data: Pick<MyData, Included>) {
    this.data = data
  }

  // noop               = DependentMethod([ 'username' ], function() {})   // Makes usernameCapitalised work
  // static static_noop = DependentMethod([ 'username' ], function() {})   // Makes usernameCapitalised work
  // noop_arrow         = DependentMethod([ 'username' ], () => undefined) // DOESN'T make usernameCapitalised work
  // static snoop_arrow = DependentMethod([ 'username' ], () => undefined) // DOESN'T make usernameCapitalised work

  usernameCapitalised = DependentMethod(
    [ 'username' ],
    function() {
      return this.data.username.toUpperCase()
    }
  )

  useridTimesTen = DependentMethod(
    [ 'userid' ],
    function() {
      return this.data.userid
    }
  )
}
interface MyData {
  username: string
  userid: number
}

const model = new MyModel({})
model.usernameCapitalised() // No error - unexpected
model.useridTimesTen() // Error - as expected 

同样奇怪的是,静态成员方法也算作一个新的 "first" 方法,因此插入静态 DependentMethod 调用也会在此处进行 usernameCapitalised 类型检查——只要它是不是箭头函数(即使我覆盖了 this 类型)。您可以通过取消注释 class 中的注释行来自己尝试。 class 之外的 DependentMethod 也没有效果,即使它们都在完全相同的类型上运行,这让我更倾向于认为这是一个错误。无论如何,静态成员似乎也被正确键入。

您的示例代码的问题在于 DependentMethod 的类型取决于 MyModel 的类型,后者具有其类型由 DependentMethod 的输出确定的属性,这取决于 MyModel 的类型,这...哎呀。该类型以编译器无法推断的方式循环。 MyModel 中的错误告诉你:

  usernameCapitalised = DependentMethod( // error!
//~~~~~~~~~~~~~~~~~~~ <--  'usernameCapitalised' implicitly has type 'any' 
//because it does not have a type annotation and is referenced directly or
//indirectly in its own initializer.
    [ 'username' ],
    function() {
      return this.data.username.toUpperCase()
    }
  )

这最终会级联到您的大部分属性,它们也有 any 类型。 MyModel 的实例行为异常这一事实并不奇怪;您几乎需要修复 MyModel 中的错误,然后才能期望编译器对其进行合理的处理。所以我认为错误发生在 "first" 方法上是一个转移注意力的事实;这很有趣,但我不会尝试通过更改第一个方法来修复它。相反,如果可能的话,我会修复潜在的循环性。


现在我不能确定示例代码是否足以代表您的用例,但假设它是:我看到每个 [=] 的 fn 参数传递到 DependentMethod MyModel 的 60=] 仅取决于 MyModeldata 属性 而不是其他属性。所以也许 DependentMethod 不应该引用 MyModel<UnionOfList<Dependencies>>,而应该只引用 {data: Pick<MyData, UnionOfList<Dependencies>>,像这样:

declare const DependentMethod: <D extends keyof MyData, R>(
  dependencies: D[],
  fn: (this: { data: Pick<MyData, D> }) => R
) => (this: { data: Pick<MyData, D> }) => R;

注意:我不担心这里DependentMethod的实现;我已将泛型类型参数更改为更传统(如果表达能力较差)的单个大写字符;并且我更改了泛型类型参数以表示键而不是键数组。现在 MyModel 没有错误:

class MyModel<K extends keyof MyData> {
  data: Pick<MyData, K>

  constructor(data: Pick<MyData, K>) {
    this.data = data
  }

  usernameCapitalised = DependentMethod(
    ['username'],
    function () {
      return this.data.username.toUpperCase()
    }
  )

  useridTimesTen = DependentMethod(
    ['userid'],
    function () {
      return this.data.userid
    }
  )
}

并且您的 MyModel 实例的行为与我假设的一样:

const emptyModel = new MyModel({})
emptyModel.usernameCapitalised(); // error
emptyModel.useridTimesTen(); // error

const usernameModel = new MyModel({ username: "Alice" });
usernameModel.usernameCapitalised(); // okay
usernameModel.useridTimesTen(); // error

const useridModel = new MyModel({ userid: 1 });
useridModel.usernameCapitalised(); // error
useridModel.useridTimesTen(); // okay

const fullModel = new MyModel({ userid: 1, username: "Alice" });
fullModel.usernameCapitalised(); // okay
fullModel.useridTimesTen(); // okay

如果发现 DependentMethod() 需要访问 MyModel 的更多属性,那么您可能需要将其重构为包含这些属性的基础 class,以及在没有它们的情况下扩展 class,并让 DependentMethod() 仅引用基数 class。这个想法是为了确保您的类型是 "grounded" 而不是循环类型。

好的,希望对您有所帮助。祝你好运!

Playground Link to code