Reputation: 1981
In ReactJS with TypeScript (both latest stable at the time of writing of this post) I can define a component that conditionally renders its children
if and only if a given property is truthy. One possible implementation could be:
// I will be using this type helper in subsequent examples
export type Maybe<T> = T | undefined | null
export function Only<T>(props: {
when: Maybe<T>
children: ReactNode
}) {
const {when, children} = props
return <>{when ? children : null}</>
}
It can then be used in the following manner:
const MyComponent = (props: { value: number }) => (
<div>Value: {props.value}</div>
);
export function MyPage(){
const [value, setValue] = useState<number>()
// value: number | undefined
setTimeout(()=>{
// Simulate a request
setValue(42)
}, 1000)
return (
<div>
<Only when={value}>
<MyComponent value={value!}/>
</Only>
</div>
)
}
It behaves as desired, but the issue is that I have to use the non-null assertion operator !
in order to pass the value
to MyComponent
. Fortunately, I am able to solve it by passing the narrowed type to children
:
export function Only<T>(props: {
when: Maybe<T>
children: ReactNode | ((when: T) => ReactNode)
}) {
const {when, children} = props;
return when ?
<>{children instanceof Function ? children(when) : children}</>
: null
}
I would then use it in the following manner:
<Only when={value}>{value => // Overshadowing with narrowed type
<MyComponent value={value}/>
}</Only>
Once again, this works as expected, albeit introducing a little bit of verbosity.
But what if I need to conditionally render a component based on "truthiness" of more than one property and pass them down to children
with narrowed types? One obvious solution is to nest the aforementioned Only
components:
const MyComponent = (props: { value1: number, value2: string }) => (
<>
<div>Value1: {props.value1}</div>
<div>Value2: {props.value2}</div>
</>
);
export function MyPage() {
const [value1, setValue1] = useState<number>()
const [value2, setValue2] = useState<string>()
setTimeout(() => {
// Simulate a request
setValue1(42)
setValue2('foo')
}, 1000)
return (
<div>
<Only when={value1}>{value1 =>
<Only when={value2}>{value2 =>
<MyComponent value1={value1} value2={value2}/>
}</Only>
}</Only>
</div>
)
}
Even though it will work with any number of properties, it is easy to see how this approach is not scalable given the sheer amount of boilerplate code we would need to write in order to pass several properties this way. Ideally, we would pass those properties in an array, whose members will be narrowed down in a similar manner. For example, a special case for 2 properties would look like this:
export function Only2<T, S>(props: {
when: [Maybe<T>, Maybe<S>]
children: ReactNode | ((when: [T, S]) => ReactNode)
}) {
const {when, children} = props
return when[0] && when[1] ?
<>{children instanceof Function ? children([when[0], when[1]]) : children}</>
: null
}
It would then be used in the following manner:
<Only2 when={[value1, value2]}>{([value1, value2]) =>
<MyComponent value1={value1} value2={value2}/>
}</Only2>
Unfortunately, I am unable to come up with a solution for a general case, where we would be able to pass and narrow down types of any number of values in a convenient manner as shown in the snippet above. Hence my question is twofold: Is it even possible to achieve the result that I am looking for in TypeScript? And if not, are there any alternative strategies for achieving a generic component that could be used in a convenient manner as was shown in the snippets that used Only
component?
Upvotes: 2
Views: 924
Reputation: 2050
You can use tuples here.
// Helper type to remove nulls from array
type RemoveNulls<T> = {
[K in keyof T]: NonNullable<T[K]>
}
// simplified example of a function
// its generic parameter is a tuple
function nonNullableArray<T extends readonly any[]>(items: T, fn: (items: RemoveNulls<T>) => void) {
if (items.some(item => item === null)) {
return;
}
// Unfortunately, we have to cast type here.
// I can't think of any other way to narrow type of array by checking it's items
fn(items as RemoveNulls<T>);
}
declare const a: string | null;
declare const b: number | null;
// `as const` is an important part. It helps typescript to narrow down type
// from array to tuple
// (string | number | null)[] vs [string | null, number | null]
const v = nonNullableArray([a, b] as const, ([a, b]) => {
console.log(a + b)
})
Upvotes: 3
Reputation: 1981
Thanks to @edemaine for his brilliant answer. In this answer I merely adapt his solution to the examples I provided in my question, just for the sake of competition, in case someone might find it useful:
import React, {ReactNode, useState} from "react"
type OrNull<T> = {
[P in keyof T]: T[P] | undefined | null
}
export function Only<T>(props: {
when: OrNull<T> | boolean // Allows usage of boolean literals
children: ReactNode | ((when: T) => ReactNode)
}) {
const {when, children} = props
if (typeof when === 'boolean') {
if (!when)
return null
} else {
for (const key in when) {
if (!when[key])
return null
}
}
return <>{children instanceof Function ? children(when as T) : children}</>
}
const MyComponent1 = (props: { value1: number }) => (
<>
<div>Value1: {props.value1}</div>
</>
)
const MyComponent23 = (props: { value2: string, value3: boolean }) => (
<>
<div>Value2: {props.value2}</div>
<div>Value3: {Boolean(props.value3).toString()}</div>
</>
)
export function MyPage() {
const [value1, setValue1] = useState<number>()
const [value2, setValue2] = useState<string>()
const [value3, setValue3] = useState<boolean>()
setTimeout(() => {
// Simulate a request
setValue1(42)
setValue2('foo')
setValue3(true)
}, 1000)
return (
<div>
<Only when={{value1, value2, value3}}>{({value1, value2, value3}) =>
<>
<MyComponent1 value1={value1}/>
<MyComponent23 value2={value2} value3={value3}/>
</>
}</Only>
</div>
)
}
Upvotes: 0
Reputation: 3120
Here's an approach that uses object types, and illustrates use with React components. I assumed that you have an object type without null
/undefined
allowed, and added that as an option.
import React, {ReactNode} from 'react';
type OrNull<T> = {
[P in keyof T]: T[P] | undefined | null;
};
function Only<T>(props: {
when: OrNull<T>,
children: ReactNode | ((when: T) => ReactNode)
}) {
const {when, children} = props;
for (const key in when) {
if (when[key] == null) return null;
}
return <>
{children instanceof Function ? children(when as T) : children}
</>;
}
type FooBar = {
foo: string,
bar: boolean,
}
const child = ({foo, bar}: FooBar) =>
<span>{foo} is {bar}</span>;
<>
<Only<FooBar> when={{foo: 'hi', bar: true}}>
{child}
</Only>
<Only<FooBar> when={{foo: 'hi', bar: undefined}}>
{child}
</Only>
</>
Upvotes: 1