You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.14 22:46:04