When to use typescript optional properties? How is it different from declaring properties as undefined?
The most important thing to understand is that “?” optional property and same property declared as required but possibly undefined mean absolutely different things.
Typescript “?” operator means that parameter is optional, it’s not necessary to pass it in component/function.
Undefined means that parameter must be passed in but its value may be undefined.
You shouldn’t use “?” as a replacement of undefined and vice versa even though “?” means that parameter may be missing and therefore will be undefined for the Typescript compiler (I want to briefly mention here that optional parameter is not a null value, it’s either exist or undefined when not passed).
Let’s take a look at an example with an optional parameter:
interface UserProps { name: string, age?: string}
const getUserData = ({name, age} : UserProps) => { if(age) { return `User ${name} is ${age}.`; } return `User ${name} decided not to tell his/her age.`;}getUserData({ name: 'Mark' });
I don’t need to pass “age” in getUserData function if I don’t want to but I also can forget about it because Typescript doesn’t highlight missing optional parameters. In more complicated cases other developers have to read the whole component’s code to understand how it works and pass data accordingly.
In addition to that we may have one more problem: if we delete “age” logic from getUserData function at all — it’s easy to forget to delete “age” from UserProps type (especially if it’s imported from another file) because Typescript accepts it as a valid variant:
interface UserProps { name: string, age?: string}const getUserData = ({name} : UserProps) => { return `User's name is ${name}`;}getUserData({ name: 'Mark' });
Now let’s look at the same function but with an explicitly stated undefined parameter:
interface UserProps { name: string, age: string | undefined}const getUserData = ({name, age} : UserProps) => { if(age) { return `User ${name} is ${age}.`; } return `User ${name} decided not to tell his/her age.`;}getUserData({ name: 'Mark', age: undefined });
In the above example Typescript will force me to pass “age” key in getUserData function (I even don’t need to look inside of component to find out about that) and will not allow me to forget to delete “age” from UserProps if I reimplement getUserData function:
interface UserProps { name: string}const getUserData = ({name} : UserProps) => { return `User's name is ${name}`;}getUserData({ name: 'Mark' });
The third problem with the optional “?” parameter is that when people use it in the interface they have to write the code that actually works without that parameter and they forget about it quite often. I noticed that, for some reason, developers tend to write checks in code if they explicitly defined parameter as undefined while forget about checking if they put parameter as optional.
Bad code:interface MyButtonProps { onClick: () => void; text?: string; isSubmit?: boolean;}const MyButton: React.FC<MyButtonProps> = ({ onClick, text, isSubmit }) => { return ( /* Developer didn't handle the case when flag is passed as true but text is not passed */
<button onClick={onClick}>{isSubmit ? text : 'Cancel'}</button>;
)};
Final problem of the typescript optional “?” parameters is that people like to use it just in case… you know, to make component more flexible in future. What usually happens is that they make it more open for bugs and wrong assumptions instead of flexibility. There are some general rules about usage of optional parameters in the code:
- Use optional parameters only if it’s really optional and component will work as expected if user does not pass it. If you want you can try to use your component yourself without passing optional prop and see how it behaves.
- If you have to use optional parameters be sure that your component will not break (write defensive code like checking if optional parameters are passed). Worth to mention that component becomes messy and unreadable if you have a lot of defensive code:
OK code:interface MyButtonProps { onClick: () => void; text?: string; isSubmit?: boolean;}const MyButton: React.FC<MyButtonProps> = ({ onClick, text, isSubmit }) => { const buttonText = text ? text : 'Submit'; /* If isSubmit is not passed then component will not break and will show Cancel button text. Not ideal and can be improved but at least component behaves sort of as expected. */ return (
<button onClick={onClick}>
{isSubmit ? buttonText : 'Cancel'}
</button>
)};
3. If all parameters are optional it may be a good time to ask what’s the point/value of this component/function if it can exist without any props. You can use your component by yourself without any prop passed to see how it looks like and is it useful at all.
4. One of the good cases where optional parameter is healthy is when it’s part of UI configuration of component (when we have some default look and feel that can be adjusted via props). We don’t want to pass isLarge as false all the time when we need the most common use case — a small button:
OK code:interface MyButtonProps { onClick: () => void; text: string; isSubmit: boolean; isLarge?: boolean;}const MyButton: React.FC<MyButtonProps> = ({ onClick, text, isSubmit, isLarge = false }) => { return (
<button onClick={onClick} className={isLarge ? 'large-btn': 'small-btn'}>
{isSubmit ? text : 'Cancel'}
</button>
)};
Conclusion:
In your application you will have both cases of optional “?” parameter and undefined type and it’s absolutely normal and depends on the design of your component. You just need to make sure that you don’t overuse an optional parameters to make your component more “flexible” because:
- it may increase other developers’ time spent on figuring out how component is supposed to work.
- it’s easy to forget to delete some optional parameters in other files if function implementation was changed because Typescript doesn’t highlight that.
- people tends to forget write defensive code for optional parameter because they assume that next developer will figure out what is expected to pass for component to work properly (as in example with buttons above: developer should figure out that it’s expected to pass “text” if we pass flag value).
- component with a lot of optional parameters may be hard to maintain and extend without missing some detail or potential use case.