Есть ли альтернатива частичному принятию только полей другого типа и ничего больше? - PullRequest
1 голос
/ 15 июня 2019

Данные интерфейсы или классы A и B с общим полем x1

interface A {
  a1: number;
  x1: number;  // <<<<
}

interface B{
  b1: number;
  x1: number;  // <<<<
}

А с учетом реализаций А и В

let a: A = {a1: 1, x1: 1};
let b: B = {b1: 1, x1: 1};

Typescript позволяет это, хотя b1 не часть A:

let partialA: Partial<A> = b;

Вы можете найти объяснение, почему это происходит здесь: Почему Partial принимает дополнительные свойства от другого типа?

Является ли альтернативой Partial принимать только поля другого типа и ничего больше (не требуя все поля хотя)? Что-то вроде StrictPartial?

Это вызывает много проблем в моей кодовой базе, поскольку просто не обнаруживает, что неправильный класс передается в качестве параметров функциям.

1 Ответ

2 голосов
/ 16 июня 2019

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

Надеюсь, это поможет; удачи!

Ссылка на код

...