То, что вы действительно хотите, называется точными типами , где что-то вроде "Exact<Partial<A>>
" предотвратит избыточные свойства при любых обстоятельствах. Но TypeScript напрямую не поддерживает точные типы (по крайней мере, не в TS3.5), поэтому нет хорошего способа представить Exact<>
как конкретный тип. Вы можете имитировать точные типы в качестве общего ограничения, что означает, что внезапно все, что с ними связано, должно стать общим, а не конкретным.
Единственный раз, когда система типов обрабатывает типы как точные, это когда проверяет избыточные свойства для "литералов свежих объектов", но в некоторых крайних случаях этого не происходит. Одним из таких крайних случаев является случай, когда ваш тип слаб (без обязательных свойств), например Partial<A>
, поэтому мы вообще не можем полагаться на избыточные проверки свойств.
И в комментарии вы сказали, что хотите класс, конструктор которого принимает аргумент типа Exact<Partial<A>>
. Что-то вроде
class Example {
constructor(public partialA: Exact<Partial<A>>) {} // doesn't compile
}
Я покажу вам, как получить что-то подобное, вместе с некоторыми оговорками по пути.
Давайте определим псевдоним универсального типа
type Exactly<T, U> = T & Record<Exclude<keyof U, keyof T>, never>;
Это принимает тип T
и кандидат тип U
, который мы хотим убедиться, что он "точно T
". Он возвращает новый тип, подобный T
, но с дополнительными never
-значными свойствами, соответствующими дополнительным свойствам в U
. Если мы используем это как ограничение для U
, например U extends Exactly<T, U>
, то мы можем гарантировать, что U
соответствует T
и не имеет дополнительных свойств.
Например, представьте, что T
- это {a: string}
, а U
- это {a: string, b: number}
. Тогда Exactly<T, U>
становится эквивалентным {a: string, b: never}
. Обратите внимание, что U extends Exactly<T, U>
имеет значение false, поскольку их свойства b
несовместимы. Единственный способ, которым U extends Exactly<T, U>
является истинным, - это если U extends T
, но не имеет дополнительных свойств.
Итак, нам нужен универсальный конструктор , что-то вроде
class Example {
partialA: Partial<A>;
constructor<T extends Exactly<Partial<A>, T>>(partialA: T) { // doesn't compile
this.partialA = partialA;
}
}
Но вы не можете этого сделать, потому что функции конструктора не могут иметь свои собственные параметры типа внутри объявлений классов. Это печальное следствие взаимодействия между универсальными классами и универсальными функциями, поэтому нам придется обойти это.
Вот три способа сделать это.
1: Сделать класс "излишне общим". Это делает конструктор универсальным по желанию, но заставляет конкретные экземпляры этого класса переносить указанный универсальный параметр:
class UnnecessarilyGeneric<T extends Exactly<Partial<A>, T>> {
partialA: Partial<A>;
constructor(partialA: T) {
this.partialA = partialA;
}
}
const gGood = new UnnecessarilyGeneric(a); // okay, but "UnnecessarilyGeneric<A>"
const gBad = new UnnecessarilyGeneric(b); // error!
// B is not assignable to {b1: never}
2: Скрыть конструктор и вместо этого использовать статическую функцию для создания экземпляров. Эта статическая функция может быть универсальной, пока класс не является:
class ConcreteButPrivateConstructor {
private constructor(public partialA: Partial<A>) {}
public static make<T extends Exactly<Partial<A>, T>>(partialA: T) {
return new ConcreteButPrivateConstructor(partialA);
}
}
const cGood = ConcreteButPrivateConstructor.make(a); // okay
const cBad = ConcreteButPrivateConstructor.make(b); // error!
// B is not assignable to {b1: never}
3: Сделать класс без точных ограничений и дать ему фиктивное имя. Затем используйте утверждение типа, чтобы создать новый конструктор класса из старого, который имеет необходимую сигнатуру общего конструктора:
class _ConcreteClassThatGetsRenamedAndAsserted {
constructor(public partialA: Partial<A>) {}
}
interface ConcreteRenamed extends _ConcreteClassThatGetsRenamedAndAsserted {}
const ConcreteRenamed = _ConcreteClassThatGetsRenamedAndAsserted as new <
T extends Exactly<Partial<A>, T>
>(
partialA: T
) => ConcreteRenamed;
const rGood = new ConcreteRenamed(a); // okay
const rBad = new ConcreteRenamed(b); // error!
// B is not assignable to {b1: never}
Все они должны работать, чтобы принимать "точные" Partial<A>
экземпляры и отклонять вещи с дополнительными свойствами. Ну, почти.
Они отклоняют параметры с известными дополнительными свойствами. Система типов на самом деле не имеет хорошего представления для точных типов, поэтому любой объект может иметь дополнительные свойства, о которых не знает компилятор. В этом суть заменяемости подклассов для суперклассов. Если я могу сделать class X {x: string}
, а затем class Y extends X {y: string}
, то каждый экземпляр Y
также является экземпляром X
, хотя X
ничего не знает о свойстве y
.
Таким образом, вы всегда можете расширить тип объекта, чтобы компилятор забыл о свойствах, и это действительно так: (избыточная проверка свойств в некоторых случаях делает это более трудным, но не здесь)
const smuggledOut: Partial<A> = b; // no error
Мы знаем, что компилируется, и я ничего не могу изменить. А это значит, что даже с реализациями, описанными выше, вы все равно можете передать B
in:
const oops = new ConcreteRenamed(smuggledOut); // accepted
Единственный способ предотвратить это - с помощью некоторой проверки во время выполнения (исследуя Object.keys(smuggledOut)
. Поэтому неплохо встроить такую проверку в конструктор класса, если действительно опасно принимать что-то с дополнительными свойствами. Или, Вы могли бы построить свой класс таким образом, чтобы он молча отбрасывал дополнительные свойства, не будучи поврежденным ими. В любом случае, приведенные выше определения классов примерно настолько, насколько система типов может быть выдвинута в направлении точных типов, по крайней мере для Теперь.
Надеюсь, это поможет; удачи!
Ссылка на код