defaultProps in your components...
UPDATE — August 2018
I’ve released rex-tils library, which includes solution discussed within this article. Check it out!
rex-tils - Type safe utils for redux actions, epics, effects and various guard utils
This post is based on TypeScript 2.9 and uses a strict mode. If you don’t use strict mode, turn it on ASAP because not using strict mode is like cheating on your girlfriend and you don’t wanna do that. right? ( if you’re in gradual migration phase from JS to TS, nonStrict is OK ! )
In this article I will demonstrate the issue and how to solve it via class Components.
So let’s define a
Button component, with following API, which will be used across this blogpost.
- onClick ( click handler )
- color ( what color will be used )
- type (button type ‘button’ or ‘submit’)
We will annotate
type as optional, because they will be defined via defaultProps, so consumer of our component doesn't have to provide those.
Now when we use it within our root App component, we get correct optional and required props compile time checking/intellisense:
Everything works and it’s typed. Beautiful. We can go home now… Well not so fast my friends!
defaultProps are not typed at all, because type checker cannot infer types from generic class extentions definition to its static properties.
What does that even mean?
- you can set anything to your
- your are defining same things twice ( types and implementation )
No type checking for defaultProps:
We can fix this by extracting
type type properties to separate type and then use type intersection by mapping our defaults to be optional via
Partial mapped type helper from TS standard library.
Then we need to explicitly annotate our
static defaultProps: DefaultProps which will get us proper type safety/DX within our defaultProps implementation!
Last thing what I tend to do, is to extract
initialState(if state is used) to separate constants, which will give us also another benefit → obtaining type definition from implementation, which introduces less boilerplate in your codebase and only one source of truth → the implementation.
So far so good.
Let’s introduce some logic to our component shall we?
Let’s say we don’t wanna use just css inline styles ( which is an antipattern/bad performance ), and based on
color prop, we wanna generate appropriate css class with some pre defined style definition.
We’ll define a
resolveColorTheme function, which will accept our color prop and as outcome we will get css className.
With this, we will get a compile error! oh no! panic!
Type 'undefined' is not assignable to type '"blue" | "green" | "red"'
Why do we get an error now? Well
color is optional, and we are in strict mode, which means, that the type union is extended by an
void type, but our function doesn't accept
undefined. This is also compiler at it's best, which tries to protect us to adhere to proper program execution ( remember the times
undefined is not a function ? ).
How to fix this a.k.a solving the million dollar problem?
As of today June 2018/TypeScript 2.9 there are 4 options how to fix this:
- Non-null assertion operator
- Component type casting
- High order function for defining defaultProps
- Props getter function
Let’s take a look at those one by one.
1. Non-null assertion operator
This solution is a no brainer, all you need to do is tell the type checker explicitly that hey dude, this won’t be null or undefined, trust me I’m an human…ehm 🤖… This is achieved by non-null assertion operator
This might be ok for simple use cases ( like small props API, accessing particular props only in render method ), but once your component starts to grow, it can get messy and confusing pretty quickly. Also you need to double check all the time which prop is defined as defaultProps -> more cognitive overhead for developer === bad DX / error prone
2. Component type casting
So how to solve our problem with mitigating all the pittfals mentioned in first solution?
We can create our component via anonymous class and assign it to constant which we will cast to final outcome component with proper prop types while keeping all “defaultProps” as defined within our implementation
This solves our former problem, but somehow it feels like a dirty hack to me.
Can we improve this somehow? Well TypeScript 2.8 introduced a very powerful feature — conditional types. Let’s use the new and shinny feature with more functional approach, shall we ?
3. High order function for defining defaultProps
We can define a factory function/high order function, for declaring defaultProps and leveraging conditional types to correctly resolve our props API.
I like this ! We don’t use React API for defining defaultProps explicitly, this is handled by our
withDefaultProps function. Also we can omit
type DefaultProps if we wan’t to and inline it within our
type Props . Everything else (type inference) is handled by TypeScript.
This is awesome!
So are we done here Martin? Of course. Hmm.. but wait… what about Generic Components???
Oh no, I completely forgot about that use case… 🤯
Excuse me ! What is a Generic Component? 👇👀
If we would like to use this pattern (withDefaultProps function) with generic props, our generic type would be lost, so if you wanna define a generic component, this solution is not feasible 😥. Besides that, all good!
So is there some ultimate pattern Martin ? 👀🧐😱
Well I don’t know if it’s ultimate, but it works and covers all previously mentioned issues.
4. Props getter function
Behold! the humble factory/closure identity function pattern with conditional types mapping.
Note that we are leveraging similar type mapping constructs like we did for
withDefaultPropsfunction except that we don't map defaultProps to be optional as they are not optional within our component implementation.
Our function creates closure and with that stores/infers defaultProps type information via generic parameter. Then the function just returns merged
defaultProps and from the runtime perspective it returns the same props that we passed, so standard React API is used for runtime props aquisition/resolution.
Also note that we are explicitly setting
childrenprop to our public Props API, which is saying that our Button needs to have one child of type ReactNode. If consumer of our component would not provide ReactNode as child, compiler would notify us by compile error. Gracias TypeScript !
Let’s use this within our component implementation !
Further explanation what’s going on:
This is very slick, don’t you think ?
We are done here, this final solution covers all former issues:
- no need for escape hatches by using non null assertion operator
- no need to cast our component to other types with more indiriection ( additional const Button )
- we don’t have to re-create component implementation and thus loosing any types in the process ( withDefaultProps function )
- works with generic components
- easy to reason about and future proof ( TypeScript 3.0 )
TypeScript 3.0 🖖
If you think that TypeScript team didn’t noticed this “million dollar problem”, you’re completely wrong 😇. Those guys love the community and trying to gives us the best JS type checker on the planet ❤️. So yeah, Daniel Rosenwasser created an issue recently about better support for default props in JSX which is targeted for TypeScript 3.0.
TypeScript will implement generic way (powered by conditional types, no magic strings or tightly coupling in compiler for specific technology/React) how to obtain default props and will reflect those within JSX, by looking up factory function definition, which is responsible for creating VirtualDom objects ( for React —
createElement , for Preact -
Also that’s one of the reasons why are we’re preserving React API for defining defaultProps in our last solution.
With that said, we are at the end of our journey for solving the million dollar problem, yay !
As always, don’t hesitate to ping me if you have any questions here or on twitter (my handle @martin_hotell) and besides that, happy type checking folks and ‘till next time! Cheers! 🤙🏄🍻