TypeScript different types for objects with the same shape

0 votes

I have an is-a relationship type of thing going on. I'm modeling this through factories that create factories, but I could have also use inheritance (except, I would like to avoid inheritance).

Here's a slightly more concrete example of what's going on:

function createShapeCategory({ getArea }) {
  const create = data => ({
    getArea: () => getArea(data),
    data,
  })

  return { create }
}

const rectangle = createShapeCategory({
  getArea: ({ width, height }) => width * height
})
const triangle = createShapeCategory({
  getArea: ({ width, height }) => width * height / 2
})

const myRectangle = rectangle.create({ width: 2, height: 3 })
const myTriangle = triangle.create({ width: 2, height: 3 })
console.log(myRectangle.getArea(), myTriangle.getArea())

 Run code snippet

Expand snippet

The question is - how do I make a Rectangle and a Triangle type? Both myRectangle and myTriangle will be the same type in the eyes of typeScript, because they have the same shape, which is a problem. What if I wanted to make some trig functions specific to triangles, that only took triangles in as a parameter? You could easily accidentally pass in a rectangle as well.

To illustrate what I'm talking about:

interface IRectangle {
  getArea: () => number
}
interface ITriangle {
  getArea: () => number
}

// ... define createShapeCategory, rectangle, and triangle ...

const myTriangle: ITriangle = triangle.create({ width: 2, height: 3 })

const circumferenceOfRect = (rect: IRectangle) => ...
circumferenceOfRect(myTriangle) // This is legal :(

Just to show that this isn't an issue directly related to my factory-of-factory approach, here's the same chunk of code, modeled using inheritance instead. It suffers from the same issues.

Show code snippet

I have come up with one possible solution, but it's pretty gross. I can attach a unique symbol to each instance, then make the symbols part of the type. This will use TypeScript's "unique symbol" feature, which is very constrained - I would have to make the symbol first, pass it into the factory, and make the symbol part of the type from outside. I can't have the factory make the symbol for me, due to the fact that you can't return something with the type "unique symbol".

Current ugly solution:

interface Shape<S extends symbol, D> {
  sentinel: S
  getArea: () => number
  data: D
}

interface createShapeCategoryOpts<S extends symbol, D> {
    sentinel: S
    getArea: (data: D) => number
}
function createShapeCategory<S extends symbol, D>({ sentinel, getArea }: createShapeCategoryOpts<S, D>) {
  const create = (data: D): Shape<S, D> => ({
    sentinel,
    getArea: () => getArea(data),
    data,
  })

  return { create }
}

const rectangleSentinel = Symbol('Rectangle')
interface RectangleData { width: number, height: number }
type Rectangle = Shape<typeof rectangleSentinel, RectangleData>
const rectangle = createShapeCategory<typeof rectangleSentinel, RectangleData>({
  sentinel: rectangleSentinel,
  getArea: ({ width, height }) => width * height
})

const triangleSentinel = Symbol('Triangle')
interface TriangleData { width: number, height: number }
type Triangle = Shape<typeof triangleSentinel, TriangleData>
const triangle = createShapeCategory<typeof triangleSentinel, TriangleData>({
  sentinel: triangleSentinel,
  getArea: ({ width, height }) => width * height / 2
})

const myRectangle = rectangle.create({ width: 2, height: 3 })
const myTriangle = triangle.create({ width: 2, height: 3 })
console.log(myRectangle.getArea(), myTriangle.getArea())

const rectOnlyFn = (rect: Rectangle) => 0
rectOnlyFn(myTriangle) // Yay! An error!

Is there a better way to do this? I feel like it should be a common problem, but I couldn't find much help on the Internet. Or, are there at least some stuff I could do to clean up that code a bit?

Jul 5, 2022 in TypeSript by Nina
• 3,060 points

edited Mar 4 7 views

No answer to this question. Be the first to respond.

Your answer

Your name to display (optional):
Privacy: Your email address will only be used for sending these notifications.
webinar REGISTER FOR FREE WEBINAR X
REGISTER NOW
webinar_success Thank you for registering Join Edureka Meetup community for 100+ Free Webinars each month JOIN MEETUP GROUP