3

Given array of inconsistent types. This may be used to dynamically rendering of html elements for example.

interface IElement {
  type: 'button' | 'input'
  content: Button & Input
}

interface Button {
  text?: string;
  backgroundColor?: string;
}

interface Input {
  value?: string;
  placeholder?: string;
}

const elements: IElement[] = [
  {
    type: 'button',
    content: {
      text: 'Start',
      backgroundColor: 'salmon'
    }
  },
  {
    type: 'input',
    content: {
      value: 'phone',
      placeholder: 'Phone'
    }
  }
]

const newElement = elements.map(element => element.content.backgroundColor)

Is there any other ways to typecast it properly depends on type property without union?

3 Answers 3

5

In Typescript the standard pattern is to use what is called a "discriminated union". Basically, where in other languages you would work with IElement values, in Typescript you try to work with a union instead. This allows Typescript to determine the proper type when you check the value of the type field in a type guard.

It might look something like this:

interface Button {
  type: 'button'
  text: string
  backgroundColor: string
}

interface Input {
  type: 'input'
  value: string
  placeholder: string
}

type ElementUnion = Button | Input

const elements: ElementUnion[] = [
  {
    type: 'button',
    text: 'Start',
    backgroundColor: 'salmon'
  },
  {
    type: 'input',
    value: 'phone',
    placeholder: 'Phone'
  }
]

function doSomething (el: ElementUnion) {
  if (el.type === 'button') {
    console.log(el.text) // TS knows this is Button
  } else if (el.type === 'input') {
    console.log(el.placeholder) // TS knows this is Input
  }
}

Note that I haven't defined any of the properties as optional or as an intersection, however Typescript still lets me use them in doSomething as long as I check the type field first. This is the power of a discriminated union.

And if you wish, you can still use the inheritance pattern at the same time:

type ElementType = 'button' | 'input'

interface IElement {
  type: ElementType
  content: unknown
}

interface Button extends IElement {
  type: 'button'
  content: {
    text: string
    backgroundColor: string
  }
}

interface Input extends IElement {
  type: 'input'
  content: {
    value: string
    placeholder: string
  }
}

type ElementUnion = Button | Input
    
function doSomething (el: ElementUnion) {
  if (el.type === 'button') {
    console.log(el.content.text) // TS knows this is Button
  } else if (el.type === 'input') {
    console.log(el.content.placeholder) // TS knows this is Input
  }
}

Typescript Playground

Sign up to request clarification or add additional context in comments.

5 Comments

Why not use generics. this looks complicated and tightly coupled??
@Sohan you need a union in order to have content typed properly. In your generic answer you are forced to define content: T1 & T2 and then set all the actual content properties as optional. That's not type safe.
This is right but the base interface IElement doesn't serve any purpose and might cause confusion
@AluanHaddad it can be useful to enforce type of the discriminator as ElementType. Also there could be code that depends on a common shape for these objects and if one doesn't match then you may want the error to be reported at point of the definition rather than use.
I agree it definitely can be useful. Anyway, I appreciate that added clarification about its optionality. A very strong answer.
2

You can use type:

type IElement = {
  type: 'button'
  content: Button
} | {
  type: 'input'
  content: Input
}

I don't know how to do it with interface.

2 Comments

Haven't tried this one yet, but this syntax looks short and concise, I like it. Hopefully it works with loops well.
Unfortunately, this way doesn't work with loops in IDE
-1

If you ask me do to proper type, i would go with Typescript Generics

Hers is the sample,

interface IElement<T> {
    type: string,
    content: T
}

interface Button {
    text?: string;
    backgroundColor?: string;
}

interface Input {
    value?: string;
    placeholder?: string;
}

const elements: IElement<Button>[] = [
    {
        type: 'button',
        content: {
            text: 'Start',
            backgroundColor: 'salmon'
        }
    }
]
const newElement = elements.map(element => element.content.backgroundColor);

If you are looking for extension with multiple classes and merge,

interface IElement<T1 , T2> {
    type: string,
    content: T1 & T2
}

interface Button {
    text?: string;
    backgroundColor?: string;
}

interface Input {
    value?: string;
    placeholder?: string;
}

const elements: IElement<Button,Input>[] = [
    {
        type: 'button',
        content: {
            text: 'Start',
            backgroundColor: 'salmon'
        }
    },
    {
        type: 'input',
        content: {
            value: 'phone',
            placeholder: 'Phone'
        }
    }
]
const newElement = elements.map(element => element.content.backgroundColor);

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.