Можно сделать ограничение для 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
.
Хорошо, надеюсь, это поможет; удачи!
Детская площадка ссылка на код