Ограничение по типу значения для типов индекса в универсальных типах TypeScript - PullRequest
0 голосов
/ 18 июня 2020

У меня есть класс:

class AdderEffect<T, K extends keyof T> {
  key: K;
  value: T[K] & number;

  constructor(key: K, value: T[K] & number) {
    this.key = key;
    this.value = addend;
  }

  apply(state: T): T {
    // do arithmetic things and return a copy
    return Object.assign({}, state, {
      [this.key]: this.value + state[this.key]
    });
  }
}

Идея в том, что T - это общий c объект, K - это ключ к этому объекту. Однако в рамках метода apply я хотел бы иметь возможность выполнять арифметическую операцию c с этим ключом. Итак, мне нужно ограничить K таким образом, чтобы связанное с ним значение тип было числом.

Использование может выглядеть так:

interface Point = {
  x: number;
  y: number;
};

const obj: Point = { x: 1, y: 2 };
const adder = new AdderEffect<Point, 'x'>('x', 3);

adder.apply(obj); // returns { x: 4, y: 2 }

тогда как

interface Animal = {
  name: string
}

const adder = new AdderEffect<Animal, 'name'>({ name: 'Fido' }, 1);

должно завершиться неудачей.

Это действительно кажется неуклюжим (особенно это <Point, 'x'>('x', 3) бизнес ...), поэтому мне любопытно, есть ли хороший способ ограничить K таким образом, чтобы T[K] extends number.

1 Ответ

1 голос
/ 18 июня 2020

Можно сделать ограничение для K, которое обеспечивает выполнение этого T[K] extends number для данного T: я обычно называю это KeysMatching, а вы пишете K extends KeysMatching<T, number>. См. этот вопрос для реализации.

Я не включаю это как ответ здесь, потому что я не думаю, что это действительно вам очень помогает. Частично потому, что компилятор не поймет, что T[KeysMatching<T, number>] будет совместим с number (это потребует более высокого порядка обобщенных рассуждений типа c, чем поддерживается в настоящее время), и вам в конечном итоге понадобится много типов утверждения двигаться вперед. Но более того: ваш AdderEffect не принимает значение типа T, пока вы не вызовете метод apply(), что заставило меня поверить, что AdderEffect должно быть только generi c в K, а не T.


Итак, я хотел бы изменить ваше ограничение ... пусть K будет любым ключом свойства, а T будет ограничен чем-то с number на T[K]. Это проще для express: T extends Record<K, number>, и компилятору легче рассуждать об этом.

И поскольку экземпляр AdderEffect на самом деле не заботится о T, пока вы не вызовете apply(), Я бы переместил T generi c из AdderEffect в метод apply(). Примерно так:

class AdderEffect<K extends PropertyKey> {
  key: K;
  value: number;

  constructor(key: K, value: number) {
    this.key = key;
    this.value = value;
  }

  apply<T extends Record<K, number>>(state: T): Omit<T, K> & { [P in K]: number } {
    return Object.assign({}, state, {
      [this.key]: this.value + state[this.key]
    });
  }

}

Я также решил сделать возвращаемое значение apply() не типом T, а типом Omit<T, K> & Record<K, number>. Это небольшое различие, но может быть важным, если вы когда-нибудь передадите тип T, где T[K] - уже , чем number, например, numeri c тип литерала или объединение числовых c литералов.


Посмотрим, как это работает. Во-первых, нам не нужно указывать какие-либо обобщения при создании AdderEffect:

const adder = new AdderEffect('x', 3);
// const adder: AdderEffect<"x">

Тип K подразумевается как "x" выше. Теперь ваш Point должен работать:

interface Point {
  x: number;
  y: number;
};
const obj: Point = { x: 1, y: 2 };
const newPoint: Point = adder.apply(obj); // okay

И что-то еще, где свойство x существует, но не является числом, не будет:

interface SomethingElse {
  x: string;
  y: number;
}
const els: SomethingElse = { x: "one", y: 2 };
adder.apply(els); // error! 
// -------> ~~~ 
// string is not assignable to number

Наконец, давайте посмотрим что происходит в случае, когда свойство x уже, чем number:

interface BinaryPoint {
  x: 0 | 1;
  y: 0 | 1;
}
const bin: Point = { x: 1, y: 1 };

const somethingNew = adder.apply(bin);
// const somethingNew: Pick<Point, "y"> & { x: number; }
// equivalent to const somethingNew: { x: number, y: 0 | 1 }

const newBinaryPoint: BinaryPoint = adder.apply(bin); // error!
// -> ~~~~~~~~~~~~~~
// number is not assignable to 0 | 1

Вы можете применить adder к BinaryPoint, но что получится больше не BinaryPoint, а значение типа { x: number, y: 0 | 1 }. Таким образом, он позволит вам вызвать его, но не позволит вам присвоить результат BinaryPoint.


Хорошо, надеюсь, это поможет; удачи!

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...