如何用TypeScript类型与类型守卫自动维护多子类型标识?
问题:基于TypeScript类型与类型守卫实现不可变Fruit类型的自动类型标识管理
补充说明
我承认之前对问题的表述不够清晰,在此致歉。
正如指出的,多数场景下我其实不需要fruitTypes,但问题在于Fruit需作为一种特殊的OOP对象变体:不可变,且不允许包含任何纯/不纯方法。
传统OOP会采用如下实现方式:
abstract class Fruit { abstract name: string } class Apple extends Fruit { name:string brand:"AppleCo"|"iApple" constructor(name:string,brand:"AppleCo"|"iApple") { super() this.name = name this.brand = brand } doAppleStuff() { if(this.brand === "iApple") { console.log("Evil company alert") this.name += ": EVIL!" return } return "DONE" } } const fruits:Fruit[] = [new Apple("GoodApple", "AppleCo") /* orange, peach, pear... */] fruits .filter((fruit) : fruit is Apple => !!fruit ) .forEach(apple=>apple.doAppleStuff())
但这种方式会产生大量可变操作与不纯函数,而我更倾向于使用不可变类型与纯函数:
// Immutable types. type Fruit = { fruitType:"Apple"|"Orange" } type Orange = Fruit & { name:string acidity:number } // Pure functions function makeAcidBomb(orange:Orange, props:{using:number}) : {orangeAfter:Orange, bomb?:Bomb} { if(orange.acidity < props.using) { console.log("Not acid enough") return {orangeAfter:{...orange}, bomb:undefined} } const newAcidity = orange.acidity - props.using return { orangeAfter: { ...orange, acidity:newAcidity }, bomb: { acidity:orange.acidity } } } const bombMaker = (orange:Orange) => makeAcidBomb(orange, {using:.25}) const orange:Orange = { fruitType:"Orange", acidity:.25, name:"bad orange", } const fruits:Fruit[] = [orange, /*apple, peach, pear...*/] const results = fruits .filter(fruit => fruit.fruitType === "Orange") .forEach(orange => bombMaker(orange as Orange))
但当代码库需要维护上百种水果类型时,手动编写大量if判断、类型比较与as类型转换会变得异常繁琐:
function *yieldEdibleSmoothieProductionProcess(fruits:Fruit[]) { yield "PRODUCTION STARTS!" for (const fruit of fruits.values()) { if(isEdible(fruit)) { yield {operation:"MIX", fruit} continue } yield {operation:"DISCARD",fruit} } yield {operation:"BLEND"} yield "DONE!" } function isEdible(fruit:Fruit, world:World) { // Fruits do not have common properties for edibility, for example fruit.canEat, // Because there might be a case where it needs complex logics to determine if it is really edible or not. // But also, they are not allowed to have methodes, so you can not rely on fruit.isEdible() either. // Because of this, you need to know what fruit it would be, then call functions that fits. if(fruit.fruitType === "Orange") return isOrangeEdible(fruit as Orange, world) if (fruit.fruitType === "Apple") return isAppleEdible(fruit as Apple) // and follows train of hundred if statements for different Fruits as well as `===` comparison and `as` castings. // if (fruit.fruitType === "Pear") // if (fruit.fruitType === "Kiwi") // if (fruit.fruitType === "Banana") // if (fruit.fruitType === "Peach") return true } // simple logic function isAppleEdible(apple:Apple) { return true // Apple is always edible! } // complex logic function isOrangeEdible(orange:Orange, world:World) { if(orange.poisonedWithAgentOrange && world.evilPatricideCompanyExists) return false return true } const fruits = [apple,orange,pear,peach,banana,melon,lemon] const smootheProcess = [...yieldEdibleSmoothieProductionProcess(fruits)]
如果使用类对象,只需借助<Fruit>泛型与instanceof检查,或实现抽象的.isEdible()方法即可轻松处理,但当前场景下我只能依赖fruitType标识。
我不愿手动维护冗长的"Apple"|"Peach"|"Pear"|"Banana"|...类型列表,避免人为错误;同时如果不维护该列表,IDE又无法提供fruitType的自动补全功能。
我开始怀疑自己的设计可能存在根本性问题,若确实如此,欢迎批评指正。
问题描述
Fruit是Apple和Orange的基础类型,定义如下:
type Fruit = { name:string, } type Apple = Fruit & { name:"apple", } type Orange = Fruit & { name:"orange", acidity:.5, }
现在我需要判断一个Fruit对象是否为Apple:
function isApple(fruit:Fruit) { if (fruit instanceof Apple) // this won't work. return "APPLE" return "NOT APPLE" }
显然instanceof无法生效,因此我必须为每个子类型添加类似“类型ID”的字段:
type Fruit = { fruitType:string, // unique id for each subtypes. name:string, } type Apple = Fruit & { fruitType:"APPLE", // unique id name:"apple", } type Orange = Fruit & { fruitType:"ORANGE", // unique id name:"orange", acidity:.5, } function isApple(fruit:Fruit) { if (fruit.fruitType === "APPLE") // success return "APPLE" return "NOT APPLE" }
但问题在于,我不得不手动维护fruitType的列表。例如使用泛型的方式:
type FruitType = "APPLE"|"ORANGE" type Fruit<FruitType> = { fruitType:FruitType, name:string, } type Apple = Fruit<"APPLE"> & { name:"apple", } type Orange = Fruit<"ORANGE"> & { name:"orange", acidity:.5, } function isApple(fruit:Fruit<FruitType>) { if (fruit.fruitType === "APPLE") return "APPLE" return "NOT APPLE" }
这种方式可行,但我更希望能实现如下效果:
// Get all types that extends `Fruit`, and gather their `type` property type. // In this case, it would be: "APPLE"|"ORANGE" type AllPossibleFruitTypes = AllPropertyTypesOfTypesThatExtends<Fruit, "type"> type Fruit = { type:AllPossibleFruitTypes, name:string, } type Apple = Fruit & { type:"APPLE", name:"apple", } type Orange = Fruit & { type:"ORANGE", name:"orange", acidity:.5, } // No need to manually update fruitTypes. function isApple(fruit:Fruit) { if (fruit.type === "APPLE") return "APPLE" return "NOT APPLE" }
核心问题
我知道可以直接使用class解决问题,但想了解是否可以仅通过类型与类型守卫实现上述需求?
内容的提问来源于stack exchange,提问作者CC-white
相关产品推荐
相关产品推荐

