javascript - ¿Existe alguna manera de proporcionar discriminación de tipos polimórfica similar a los enums de Rust?
Recientemente he vuelto a JS/TS después de programar mucho en Rust y esta es una característica que realmente extraño.
Entonces supongamos que tengo un valor que puede ser una instancia de Cat o una instancia de Dog y dependiendo de la clase a la que pertenezca, quiero realizar una lógica diferente. Dado que Javascript no puede hacer classname == (¿creo?), lo que usualmente hago es también pasar una cadena que indique la clase de esta manera:
function groomAnimal(animal: Cat | Dog, animalType: 'cat' | 'dog') {
if (animalType === 'cat') {
(animal as Cat).trimNails();
} else {
(animal as Dog).trimFur();
}
}
Odio esto porque es engorroso y el compilador no puede garantizar que el llamador cumpla correctamente con el contrato. Por otro lado, en Rust esto sería bastante elegante:
enum Animal {
Cat(CatData),
Dog(DogData),
}
fn groomAnimal(animal: Animal) {
match animal {
Cat(catData) => catData.trimNails(),
Dog(dogData) => dogData.trimFur(),
}
}
Básicamente, el enum en sí mismo, que indica la "clase", también contiene los datos de la "clase" y, por lo tanto, obtener los datos y verificar el tipo de datos son la misma operación
¿Hay algún patrón en TypeScript que permita firmas de funciones polimórficas mientras se asegura en tiempo de compilación que el código opere sobre los tipos de datos correctos?
Respuesta #1
Mantener un valor separado que indique el tipo es muy propenso a errores. Si Cat y Dog son tipos de clase (no solo interfaces), puedes usar instanceof para verificar su tipo directamente:
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();
}
}
Respuesta #2
Podrías crear una estructura componible con Cat/Dog y algún atributo identificador, un poco similar a tu enfoque pero diferente.
Algo así,
type Animal = Cat & { animalType: 'Cat' } | Dog & { animalType: 'Dog' }
Tu método groomAnimal ahora tomaría la estructura compuesta,
function groomAnimal(animal: Animal) {
if (animal.animalType === 'Cat') {
animal.trimNails();
} else {
animal.trimFur();
}
}
Aquí hay una pequeña muestra donde creo un gato y lo paso al método groomAnimal,
const tom = {
trimNails() {
console.log('Trimming nails')
}
}
groomAnimal({ ...tom, kind: 'Cat'})
En tu caso, dijiste que analizas estos a partir de algunas llamadas a la API. Ya podrías preparar tu objeto Animal/Animals[] al analizar y usar eso más tarde al asear.
Y, por supuesto, el compilador puede garantizar que el llamador realmente cumple correctamente con el contrato, como se puede ver en el código.
