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
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
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
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