Home > Mobile >  How to correctly type an object with alternate keys in TypeScript?
How to correctly type an object with alternate keys in TypeScript?

Time:10-01

I want to create a TypeScript type that involves a set of fixed keys, plus a set of alternate groups of keys, such as:

type Props = {
   id: string
} & (
{
  height: number, 
  width: number
} 
  | {color: string}
)

Now, in this case, it correctly enforces that each of the groups have all the properties of that group, or none of them, i.e.: I can't use height without also using width.

Now, if I want to also allow an alternative without any of the optional keys, the only option that I know actually gives the desired result is by using {} as an alternative.

But lint rule @typescript-eslint/ban-types disallows that, saying that depending on the use case I should either Record<string, unknown>, unknown or Record<string, never>.

But none of these really work for this case. If I use unknown or Record<string, never> or object TS doesn't allow any of the keys in the alternatives. If I use Record<string, unknown> it doesn't allow any key unless I fill one of the alternative key groups.

Is there another way of doing this that I'm missing, or should I ignore the lint rule to achieve this?

CodePudding user response:

I find in React that overloads a better case for this.

You can create an overload for each pattern that you want to support.

import React from 'react'

type RequiredProps = { id: string }

type SizeProps = { height: number, width: number } 
type ColorProps = { color: string }
type AllOptionProps = SizeProps | ColorProps


function MyComponent(props: RequiredProps): JSX.Element
function MyComponent(props: RequiredProps & SizeProps): JSX.Element
function MyComponent(props: RequiredProps & ColorProps): JSX.Element

function MyComponent(props: RequiredProps & Partial<AllOptionProps>) {
    console.log(props.id)
    if ('width' in props) console.log(props.width)
    if ('color' in props) console.log(props.color)
    return <></>
}

const a = <MyComponent id='abc' />
const b = <MyComponent id='abc' width={50} height={100} />
const c = <MyComponent id='abc' color='red' />

const d = <MyComponent id='abc' width={50} /> // error

See Playground


A second approach is to create a union where all props are in all members, but are forced to undefined if you say you can have them.

import React from 'react'

type RequiredProps = { id: string }

type NoOptionsProps = {
    height?: undefined,
    width?: undefined,
    color?: undefined
}
type SizeProps = {
    height: number,
    width: number,
    color?: undefined
} 
type ColorProps = {
    height?: undefined,
    width?: undefined,
    color: string
}
type Props = RequiredProps & (NoOptionsProps | SizeProps | ColorProps)

function MyComponent(props: Props) {
    console.log(props.id)
    console.log(props.width)
    console.log(props.color)
    return <></>
}

const a = <MyComponent id='abc' />
const b = <MyComponent id='abc' width={50} height={100} />
const c = <MyComponent id='abc' color='red' />

const d = <MyComponent id='abc' width={50} /> // error

See Playground

CodePudding user response:

You can try intersection of FC:

import React, { FC } from 'react'

interface Base {
  id: string
}

interface WithRect extends Base {
  height: number,
  width: number
}

interface WithColor extends Base {
  color: string
}


type Result = FC<WithColor> & FC<WithRect> & FC<Base>

const App: Result = (props) => <p></p>

const jsx = <App id="hello" color="green" /> // ok
const jsx____ = <App id="hello" /> // ok
const jsx___ = <App id="hello" width={1} height={1} /> // ok


const jsx_ = <App id="hello" color="green" height={1} /> // expected error
const jsx__ = <App id="hello" height={1} /> // expected error

Playground

This behavior is similar to function overloading, I would even say 95% similar but not equal.

If you are interested in typing React component props, see my articles my blog and dev.to

  • Related