Есть ли способ обеспечить различение полиморфных типов, аналогичное перечислениям Rust?

#javascript #typescript #polymorphism

#javascript #typescript #полиморфизм

Вопрос:

Недавно я вернулся к JS / TS после того, как много кодировал в Rust, и это функция, по которой я действительно скучаю.

Итак, предположим, что у меня есть значение, которое может быть экземпляром Cat или экземпляром Dog , и в зависимости от того, из какого класса оно, я хочу выполнить другую логику. Поскольку Javascript не может этого сделать classname == (я думаю?), То, на что я обычно полагаюсь, также передает строку, указывающую класс следующим образом:

 function groomAnimal(animal: Cat | Dog, animalType: 'cat' | 'dog') {
    if (animalType === 'cat') {
        (animal as Cat).trimNails();
    } else {
        (animal as Dog).trimFur();
    }
}
 

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

 enum Animal {
    Cat(CatData),
    Dog(DogData),
}

fn groomAnimal(animal: Animal) {
    match animal {
        Cat(catData) => catData.trimNails(),
        Dog(dogData) => dogData.trimFur(),
    }
}
 

По сути, enum сам по себе, который указывает «класс», также содержит данные «класса», и, таким образом, получение данных и проверка типа данных — это одна и та же операция

Есть ли в typescript какой-либо шаблон, который допускает сигнатуры полиморфных функций, гарантируя при компиляции, что код работает с правильными типами данных?

Ответ №1:

Сохранение отдельного значения, указывающего тип, очень подвержено ошибкам. Если Cat и Dog являются типами классов (а не только интерфейсами), вы можете использовать instanceof для прямой проверки его типа:

 class Cat {
    trimNails() {}
}

class Dog {
    trimFur() {}
}

function groomAnimal(animal: Cat | Dog) {
    if (animal instanceof Cat) {
        // animal must be Cat, so you can call trimNails()
        animal.trimNails();
    } else {
        // animal must otherwise be a Dog, so you can call trimFur()
        animal.trimFur();
    }
}
 

Комментарии:

1. К сожалению, они являются интерфейсами. Они анализируются из JSON, загруженного с сервера. Я не думаю, что есть простой способ сделать их классами без каких-либо накладных расходов

2. Но на самом деле, это дает мне идею. Я мог бы просто обернуть интерфейсы в классы и использовать instanceof , что на самом деле очень похоже на решение rust. Я думаю, что это лучше, чем то, что я делал

Ответ №2:

Вы могли бы создать составную структуру с Cat/Dog некоторым атрибутом идентификатора и некоторым атрибутом идентификатора, немного похожим на ваш подход, но по-другому.

Что-то вроде этого,

 type Animal = Cat amp; { animalType: 'Cat' } | Dog amp; { animalType: 'Dog' }
 

Теперь ваш groomAnimal метод будет принимать составленную структуру,

 function groomAnimal(animal: Animal) {
    if (animal.animalType === 'Cat') {
        animal.trimNails();
    } else {
       animal.trimFur();
    }
}
 

Вот один небольшой пример, в котором я создаю cat и передаю его groomAnimal методу,

 const tom = {
    trimNails() {
        console.log('Trimming nails')
    }
}

groomAnimal({ ...tom, kind: 'Cat'})
 

В вашем случае вы сказали, что анализируете их из некоторых вызовов api. Вы уже могли подготовить свой Animal/Animals[] объект при синтаксическом анализе и использовать его позже при подготовке.

И, конечно, компилятор может гарантировать, что вызывающий абонент действительно правильно выполняет контракт, как вы можете видеть из кода.

Комментарии:

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