Typescript interface with conditional types based on a sub-property

Viewed 96

want to create an interface with conditional types based on a sub-property, like the one in the below example (here the Typescript playground), but into my test function I receive these two errors:

Property 'foo' does not exist on type 'BmwMetadata | AudiMetadata'. Property 'foo' does not exist on type 'AudiMetadata'.

Property 'bar' does not exist on type 'BmwMetadata | AudiMetadata'. Property 'bar' does not exist on type 'BmwMetadata'.

Could someone help me?

type BrandName = 'bmw' | 'audi' | 'ford'

interface Brand<T = BrandName> {
  name: T
}

interface BmwMetadata {
  foo: number
}

interface AudiMetadata {
  bar: number
}

interface CarBase<T = BrandName, M = null> {
  brand: Brand<T>
  metadata: M
}

type Bmw = CarBase<'bmw', BmwMetadata>
type Audi = CarBase<'audi', AudiMetadata>
type Ford = CarBase<'ford'>

export type Car = Bmw | Audi | Ford

const test = (car: Car) => {
    if (car.brand.name === 'bmw') {
        return car.metadata?.foo // <-- error 
    }

    if (car.brand.name === 'audi') {
        return car.metadata?.bar // <-- error 
    }

    return false
}
1 Answers

Make your car classes concrete, then you can use instanceof to assert which type of car the class is.

I still challenge you to find a way to do this that involves less 'layers' of typing. Right now this is quite confusing.

type BrandName = 'bmw' | 'audi' | 'ford'

interface Brand<T = BrandName> {
    name: T
  }
  
  interface BmwMetadata {
    foo: number
  }
  
  interface AudiMetadata {
    bar: number
  }
  
  interface CarBase<T = BrandName, M = undefined> {
    brand: Brand<T>
    metadata?: M
  }
  
  class Bmw implements CarBase<'bmw', BmwMetadata> {
      brand: Brand<'bmw'>
      metadata: BmwMetadata
}
  class Audi implements CarBase<'audi', AudiMetadata> {
      brand: Brand<'audi'>
      metadata: AudiMetadata
}
  class Ford implements CarBase<'ford', undefined> {
      brand: Brand<'ford'>
      metadata: null
}
  
  const test = (car: CarBase<any,any>) => {
      if (car instanceof Bmw) {
          return car.metadata.foo // <-- no error, no null check
      }
  
      if (car instanceof Audi) {
          return car.metadata.bar // <-- no error, no null check
      }
  
      return false
  }

I think I may suggest some changes like this, but it could still be workshopped more.

enum Brand {
    Bmw,
    Audi,
    Ford
}  
  interface BmwMetadata {
    foo: number
  }
  
  interface AudiMetadata {
    bar: number
  }
  
  interface CarBase<M = undefined> {
    brand: Brand
    metadata?: M
  }
  
  class Bmw implements CarBase<BmwMetadata> {
      brand: Brand.Bmw
      metadata: BmwMetadata
}
  class Audi implements CarBase<AudiMetadata> {
      brand: Brand.Audi
      metadata: AudiMetadata
}
  class Ford implements CarBase<undefined> {
      brand: Brand.Ford
      metadata: undefined
}
  
  const test = (car: CarBase<any>) => {
      if (car instanceof Bmw) {
          return car.metadata.foo // <-- no error , no null check
      }
  
      if (car instanceof Audi) {
          return car.metadata.bar // <-- no error, no null check 
      }
  
      return false
  }
Related