简体   繁体   中英

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

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, ie: 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?

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

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM