TypeScript Quirks: Conditionally required stuff
Say you have a component that should only render some content, provided as a prop, if another prop is true.
interface IProps {
renderContent?: boolean;
content?: string;
}
const Component = ({renderContent, content}: IProps) => (
<div>
{renderContent && content}
</div>
);
In this case we could tell the component to render its content even if we’ve given it no content, so the following would be fine, but render nothing.
<Component renderContent />
What we can do, though, is define a couple of interfaces to tell it that content
is required, when renderContent
is truthy.
interface IDontRenderContentProps {
renderContent?: false; // false or not required
content?: void; // nothing
}
interface IRenderContentProps {
renderContent: true;
content: string; // Required when renderContent is true
}
We can then set these as the interface, like so.
const Component = ({renderContent, content}: IDontRenderContentProps | IRenderContentProps) => (
<div>
{renderContent && content}
</div>
);
Now we can only call it with both props, or neither.
<Component /> // Happy
<Component renderContent content="content" /> // Happy
<Component renderContent /> // Not happy
<Component content="content" /> // Not happy
This could be more readable by creating a type that combines the interfaces.
type IProps = IDontRenderContentProps | IRenderContentProps;
const Component = ({renderContent, content}: IProps) => (
<div>
{renderContent && content}
</div>
);
But what if we want to include some other properties that don’t rely on eachother? Well, how about another interface?
interface IRegularProps {
foo: string;
}
type IProps = IRegularProps & (IDontRenderContentProps | IRenderContentProps);
Now foo
is always required, renderContent
is optional, and content
is required if renderContent
is true.
Something very similar can also be done with functions, classes, and methods, using overloads.
Imagine that we have an object that only ever has string keys and string values. If we define a getter function like so:
function get (obj: {[index: string]: string}, key: string, defaultValue?: string) {
const value = obj[key];
return typeof value === 'undefined' ? defaultValue : value;
}
The return type of this function will be inferred as string | undefined
, even though if we are passing a default value we should be guaranteed to get a string value.
const thing = get({foo: 'bar'}, 'foo'); // string | undefined
Using the trick that we just learnt above will not solve the problem here because we’d just be explicitly setting the return type to string | undefined
. This is where overloads come in to play.
First of all, let’s abstract that object type, as we’re going to reuse it.
interface IStringObj {
[index: string]: string;
}
Now we can use this to define our overloads in the following way:
function get (obj: IStringObj, key: string): string | undefined;
function get (obj: IStringObj, key: string, defaultValue: string): string;
function get (obj: IStringObj, key: string, defaultValue?: string) {
const value = obj[key];
return typeof value === 'undefined' ? defaultValue : value;
}
Here we’ve defined a function, that can take either obj
and key
, and return string | undefined
, or take obj
, key
and defaultValue
and return a guaranteed string
.
Now when we call our function we get the types we’d expect.
const thing = get({foo: 'bar'}, 'foo'); // string | undefined
const thing = get({foo: 'bar'}, 'foo', 'baz'); // string
You can also define overloads as interfaces, and use them for classes and class methods. You should go do some reading on them because they are awesome!
Here’s a final example of how to use overloads to say that one parameter is required if another is present.
function maybeLogSomething (): void;
function maybeLogSomething (really: true, text: string): void;
function maybeLogSomething (really?: boolean, text?: string) {
console.log(text);
}
maybeLogSomething(); // Happy
maybeLogSomething(true, 'foo'); // Happy
maybeLogSomething(false, 'bar'); // Not happy, first value must be true if present
maybeLogSomething(true); // Not happy, requires either 0 or 2 arguments