Первый определенный метод в классе TypeScript неправильно набран (но все после него), используя функции с типизированным this - PullRequest
0 голосов
/ 05 января 2020

Я действительно заблудился из-за того, что здесь может пойти не так. По сути, я пытаюсь создать класс MyModel, который получает некоторые данные и инициализирует свои собственные значения, а также определяет некоторые методы, которые работают с этими данными. Однако модель может получить только определенную часть данных - поэтому я определяю MyModel<Included extends keyof MyData> с помощью data: Pick<MyData, Included>. Теперь я хочу, чтобы методы, которые работают с этим подмножеством MyData, могли вызываться из некоторого экземпляра модели, поэтому методы-члены определяются через вспомогательную функцию DependentMethod([ ...dependencies], method), которая возвращает функцию, которая требует правильного this - ie , MyModel с определенными атрибутами в его Included (это также добавляет некоторые средства защиты во время выполнения, поэтому он возвращает undefined, если он вызван с неправильным this в любом случае).

Затем я определяю usernameCapitalised = DependentMethod([ 'username' ], ...) et c., Что прекрасно работает ... за исключением того, что в классе независимо от того, какая первая определенная функция-член через 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 

Что также любопытно, так это то, что что методы-члены stati c также считаются новым "первым" методом, поэтому при вставке вызова stati c DependentMethod здесь также выполняется проверка типов usernameCapitalised, если это не функция стрелки (хотя я переопределяю в любом случае this). Вы можете попробовать это самостоятельно, раскомментировав закомментированные строки в классе. DependentMethod s вне класса также не имеют эффекта, хотя они оба работают с одинаковыми типами, что заставляет меня еще больше склоняться к тому, что это ошибка. Члены stati c также, похоже, набираются правильно, несмотря ни на что.

1 Ответ

1 голос
/ 06 января 2020

Проблема с вашим примером кода состоит в том, что тип 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, прежде чем вы сможете ожидать, что компилятор сделает с ним разумные вещи. Таким образом, я считаю, что ошибка в первом методе связана с красной сельдью; это интересно, но я бы не стал это исправлять, изменив метод первым. Вместо этого я бы по возможности исправил основную округлость.


Теперь я не могу быть уверен, что пример кода в достаточной мере отражает ваш вариант использования, но предполагая, что это так: я вижу, что * Параметр 1015 *, передаваемый в DependentMethod для каждого свойства MyModel, зависит только от свойства data MyModel, а не от других свойств. Поэтому, возможно, 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 здесь ; Я изменил параметры типа generi c на более обычный (хотя и менее выразительный) символ в верхнем регистре; и я изменил параметр типа generi c, чтобы он представлял ключи, а не массив ключей. Теперь 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, тогда вам может потребоваться преобразовать его в базовый класс с такими свойствами в них и в расширяемый класс без них, и DependentMethod() будет ссылаться только на базовый класс. Идея состоит в том, чтобы убедиться, что ваши типы «заземлены», а не циклически.

Хорошо, надеюсь, это поможет вам определить направление. Удачи!

Детская площадка Ссылка на код

...