Сужение типа для частичного не работает

У меня есть довольно простой класс с именем Animal. Он может принимать в качестве аргумента конструктора простой объект с соответствующими именами/типами свойств:

class Animal {
  kingdom:string;
  species:string;

  constructor(config?:Partial<Animal>) {
    this.kingdom = config?.kingdom ?? "";
    this.species = config?.species ?? "";
  }
}

Пока ничего особо сложного.

Но позже у меня есть множество животных. Моя функция добавления животных в массив будет принимать либо экземпляр Animal, либо объект конфигурации Animal, например:

const animals:Animal[] = [];
function addAnimal(animal:Animal|Partial<Animal>) {
  if (!(animal instanceof Animal)) animal = new Animal(animal);
  animals.push(animal);
}

Этот код абсолютно гарантирует, что переменная animal будет ссылаться на экземпляр Animal, но я получаю следующую ошибку компилятора:

Argument of type 'Animal | Partial<Animal>' is not assignable to parameter of type 'Animal'.
  Type 'Partial<Animal>' is not assignable to type 'Animal'.
    Types of property 'kingdom' are incompatible.
      Type 'string | undefined' is not assignable to type 'string'.
        Type 'undefined' is not assignable to type 'string'.

(Дополнительные сведения см. в Детская площадка)

Я понимаю, что Animal и Partial<Animal> очень похожи, но я надеялся, что явное присвоение сузит мою переменную animal до Animal. Сам код в порядке, так что это просто борьба с TypeScript.

Я пробовал пару разных (менее пикантных) обходных путей, в том числе:

  1. Добавление частной переменной в Animal (чтобы попытаться отличить ее от Partial)

    class Animal {
      kingdom:string;
      species:string;
      private foo:unknown;
    
      constructor(config?:Partial<Animal>) {
        this.kingdom = config?.kingdom ?? "";
        this.species = config?.species ?? "";
      }
    }
    

    Это просто меняет сообщение об ошибке на:

    Argument of type 'Animal | Partial<Animal>' is not assignable to parameter of type 'Animal'.
      Property 'foo' is missing in type 'Partial<Animal>' but required in type 'Animal'.
    
  2. Явный состав:

    if (!(animal instanceof Animal)) animal = new Animal(animal) as Animal;
    animals.push(animal);
    
  3. И даже полностью удалить оператор if:

    animal = new Animal(animal);
    animals.push(animal);
    

Все эти решения либо не действуют, либо немного изменяют сообщение об ошибке, но основная проблема остается. TypeScript не обнаруживает, что я сузил тип с Partial<Animal>|Animal до Animal.

Единственное решение, которое я нашел, которое работает до сих пор, - это явная вторая проверка:

if (!(animal instanceof Animal)) animal = new Animal(animal);
if ((animal instanceof Animal)) animals.push(animal);

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

Как я могу разумно сузить тип только до Animal, или это ограничение TypeScript? Если возможно, предпочтение отдается авторитетному источнику.

В чем разница между Promise и Observable?
В чем разница между Promise и Observable?
Разберитесь в этом вопросе, и вы значительно повысите уровень своей компетенции.
Создание собственной системы электронной коммерции на базе Keystone.js - настройка среды и базовые модели
Создание собственной системы электронной коммерции на базе Keystone.js - настройка среды и базовые модели
Прошлая статья была первой из цикла статей о создании системы электронной коммерции с использованием Keystone.js, и она была посвящена главным образом...
0
0
29
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

К сожалению, я не думаю, что вы можете заставить TypeScript сузить эту переменную, когда вы повторно используете ее таким образом. Однако есть и другие конструкции, которые устранят ошибку, вот некоторые из них:


  • Добавьте свой ручной слепок в строку push.

Это работает, поскольку в этой строке происходит несоответствие типов: присваивая Animal | Partial<Animal> позиции, ожидающей только Animal.

function addAnimal(animal:Animal|Partial<Animal>) {
  if (!(animal instanceof Animal)) animal = new Animal(animal);
  animals.push(animal as Animal);
}

  • Используйте встроенное преобразование с тернарным оператором.

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

function addAnimal(animal:Animal|Partial<Animal>) {
  animals.push(animal instanceof Animal ? animal : new Animal(animal));
}

  • Используйте комментарий @ts-ignore.

Мой goto, когда TypeScript перестает мне помогать и начинает мешать. Конечно, это игнорирует любую ошибку, которая может появиться; Я бы использовал его только тогда, когда другие обходные пути слишком обременительны.

function addAnimal(animal:Animal|Partial<Animal>) {
  if (!(animal instanceof Animal)) animal = new Animal(animal);
  // @ts-ignore: See above line, `animal` is always type `Animal` here.
  animals.push(animal);
}

Я запретил явное приведение типов, поэтому для первого варианта в любом случае потребуется третий. Тем не менее, я ценю подтверждение. Я подумал, что это будет ответ, но я хотел убедиться, что не упустил ничего очевидного.

JDB 17.05.2022 04:20

Принятие этого ответа, потому что я в основном использовал модифицированную версию второго предложения.

JDB 17.05.2022 20:36

Как упоминалось в ответ CRice, кажется, что TypeScript не может сузить переменная.

Решение, к которому я пришел, заключалось в создании второй переменной, хотя оно очень похоже на второе предложение CRice.

Я переименовал аргумент функции в animalConfig и создал новую переменную с именем animal, которая просто набирается как Animal. Затем я использую тернарный оператор (снова рекомендованный CRice) с проверкой instanceof, чтобы либо назначить animalConfig непосредственно animal, если это экземпляр класса Animal, либо создать новый экземпляр Animal, передав animalConfig в качестве аргумента.

const animals:Animal[] = [];
function addAnimal(animalConfig:Animal|Partial<Animal>) {
  const animal = (animalConfig instanceof Animal)
    ? animalConfig
    : new Animal(animalConfig);
  animals.push(animal);
}

Это позволяет избежать @ts-ignore (что может скрыть другие ошибки), явного приведения типов (которое опять же может скрывать ошибки по мере развития кода) и множественных проверок типов (что неэффективно в коде времени выполнения).

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

Другие вопросы по теме