10++ TypeScript Pro tips/patterns with (or without) React

Martin Hochel
14 min readOct 29, 2018

--

🎒 this article uses following library versions:

{
"@types/react": "16.4.16",
"@types/react-dom": "16.0.9",
"typescript": "3.1.3",
"react": "16.5.2",
"react-dom": "16.5.2"
}

🎮 source code can be found on my github profile

TypeScript is definitely the best thing that happened to JavaScript. period.

Unfortunately, I cannot say the same about “The best thing that happened to Java/C# devs writing JavaScript with it 👀😳🌀⏱”

Why 🤨?

Well, it definitely makes your Java/C# alter ego feel like home, having types within JavaScript (which is amazing !), but then, it introduces other “language features” which are not part of standard JavaScript, and because of those, it may throw a false prejudice about TypeScript, by putting it to a “Completely new language” bag, which isn’t really true IMHO.

I’ve been always trying to stay away from various TS features (for a good reasons 👉explained in this article) to stay in Idiomatic/Standard JavaScript land as much as possible.

This article describes various patterns/tips that I “invented/learned” and have been using while using TypeScript and React for building UI’s.

NOTE:

Initially, this blog post introduced “only” 10 tips, During review of this post I already added 8 more 💪. I may add additional ones in the future as React patterns/TS capabilities change/improve/evolve. Make sure to check this post time to time for any updates 😎

UPDATE:

26.1.2019 updated tip

#9 👉Use type inference for defining Component State or DefaultProps

23.1.2019 added tip

#19 👉 Use type alias instead of interface for declaring Props/State

#20 👉 Don’t use FunctionComponent<P> to define function component

Whole article is written like an “style guide” with 3 sub-sections for every tip/pattern which consists of:

  • Don’t 🚨 ( code example what you shouldn’t be doing)
  • Do ✅ or Good/Better/Consider (code example what you should be doing)
  • Why 🧐 (reasoning/explanation)

With that covered, let’s hop into 10++ TypeScript Pro tips/patterns with ( or without ) React.

1. Don’t use public accessor within classes

Don’t:

Do:

Why?

All members within class are public by default (and always public in runtime, TS private/protected will "hide" particular class properties/methods only during compile time). Don't introduce extra churn to your codebase. Also using publicaccessor is not "valid/idiomatic javascript"

2. Don’t use private accessor within Component class

Don’t:

Good:

Better:

Why:

private accessor won't make your properties/methods on class private during runtime. It's just TypeScript "emulation during compile time". Don't get fooled and make things "private" by using well known patterns like:

In reality, you should almost never need to work directly with React Component instance nor accessing its class properties.

3. Don’t use protected accessor within Component class

Don’t:

Do:

Why:

Using protected is an immediate "RED ALERT" 🚨🚨🚨 in terms of functional patterns leverage with React. There are more effective patterns like this for extending behaviour of some component. You can use:

  • just extract the logic to separate component and use it as seen above
  • HoC (high order function) and functional composition.
  • CaaF ( children as a function )

4. Don’t use enum

Don’t:

Good:

If you need to support runtime enums use following pattern:

Better:

If you don’t need to support runtime enums, all you need to use are type literals:

Why?:

To use enum within TypeScript might be very tempting, especially if you're coming from language like C# or Java. But there are better ways how to interpret both with well known JS idiomatic patterns or as can be seen in "Better" example just by using compile time type literals.

  • Enums compiled output generates unnecessary boilerplate (which can be mitigated with const enum though. Also string enums are better in this one)
  • Non string Enums don’t narrow to proper number type literal, which can introduce unhandled bug within your app
  • It’s not standard/idiomatic JavaScript (although enum is reserved word in ECMA standard)
  • Cannot be used with babel for transpiling 👀

🙇‍Enum helper

In our “Good” example, you might think like, ugh that’s a lot of boilerplate dude! I hear you my friends. Loud and clear 🙏

If you need to support runtime enums for various reasons, you can leverage small utility function from rex-tils library like showcased here:

5. Don’t use constructor for class Components

Don’t:

Do:

Why:

There is really no need to use constructor within React Component.

If you do so, you need to provide more code boilerplate and also need to call super with provided props ( if you forget to pass props to your super, your component will contain bugs as props will not be propagated correctly).

But… but… hey ! React official docs use constructor!

👉 That’s fine (React team uses current version of JS to showcase stuff)

But… but…, class properties are not standard JavaScript!

👉 Class fields are in Stage 3, which means they are gonna be implemented in JS soon

Initializing state with some logic

Of course you may ask, what if I need to introduce some logic to initialize component state, or even to initialize the state from some dependant values, like props for example.

Answer to your question is rather straightforward.

Just define a pure function outside the component with your custom logic (as a “side effect” you’ll get easily tested code as well 👌).

6. Don’t use decorators for class Components

Don’t:

Good:

Better:

Why:

Decorators are parasitic 🐛 👀 🤢

  • You won’t be able to get original/clean version of your class.
  • TypeScript uses old version of decorator proposal which isn’t gonna be implemented in ECMAscript standard 🚨.
  • It adds additional runtime code and processing time execution to your app.
  • What is most important though, in terms of type checking within JSX, is, that decorators don’t extend class type definition. That means (in our example), that our Container component, will have absolutely no type information for consumer about added/removed props.

7. Use lookup types for accessing component State/Props types

🙇‍ lookup types

Don’t:

Do:

Why:

  • Exporting Props or State from your component implementation is making your API surface bigger.
  • You should always ask a question, why consumers of your component should be able to import explicit State/Props type? If they really need that, they can always access it via type lookup functionality. So cleaner API but type information is still there for everyone. Win Win 💪
  • If you need to provide a complex Props type though, it should be extracted to models/types file exported as Public API.

8. Always provide explicit type for children Props

Don’t:

Do:

Why:

  • children prop is annotated as optional within both Component and Functional Component in react.d.ts which just mirrors the implementation how React handles children. While that's ok and everything, I prefer to be explicit with component API.
  • if you plan to use children for content projection, make sure to explicit annotate it with type of your choosing and in opposite, if your component doesn't use it, prevent it's usage with never type.

Children type constraint 🚸

Hey, mister Skateboarder ! I have a question ✋:

What types can be used for children annotation in TypeScript ? I mean, can I constraint children to be only a particular type of Component (like is possible with Flow) ? Something like Tab within Tabs children: Tab[] ?

Unfortunately not 🙃, as TypeScript isn’t able to “parse” output of JSX.factory 👉 React.createElement which returns JSX.Element from global namespace, which extends React.ReactElement<any> so what compiler gets is an object type, with type checking turned off (WARNING:every time you any a kitten dies 🙀😅)

Or as stated in TypeScript docs 👀:

“By default the result of a JSX expression is typed as any. You can customize the type by specifying the JSX.Element interface. However, it is not possible to retrieve type information about the element, attributes or children of the JSX from this interface. It is a black box ⬛️ 📦."

NOTE:

TS 2.8 introduced locally scoped JSX namespaces, which may help to resolve this feature in the future. Watch this space!

We can use following types for annotating children:

  • ReactNode | ReactChild | ReactElement
  • object | {[key:string]:unknown} | MyModel
  • primitives string | number | boolean
  • Array<T> where T can be any of former
  • never | null | undefined ( null and undefined doesn't make much sense though )

9. Use type inference for defining Component State or DefaultProps

Don’t:

Good:

Better:

By making freezing initialState/defaultProps, type system will infer correct readonly types (when someone would accidentally set some value, he would get compile error). Also marking both static defaultProps and state as readonly within the class, is a nice touch, to prevent us from making any runtime errors when incorrectly setting state via this.state = {...}

Why:

  • Type information is always synced with implementation as source of truth is only one thing 👉 THE IMPLEMENTATION! 💙
  • Less type boilerplate
  • More readable code
  • by adding readonly modifier and freezing the object, any mutation within your component will immediately end with compile error, which will prevent any runtime error = happy consumers of your app!

What if I wanna use more complicated type within state or default props?

Use as operator to cast your properties within the constant.

Example:

How to infer state type if I wanna use derived state from props?

Easy 😎… We will use pattern introduced in tip no. 5 with power of conditional types (in particular, standard lib.d.ts ReturnTypemapped type, which infers return type of any function ✌️)

10. When using function factories instead of classes for models/entities, leverage declaration merging by exporting both type and implementation

Don’t:

Do:

Why:

  • Less Boilerplate
  • One token for both type and implementation / Smaller API
  • Both type and implementation are in sync and most importantly, implementation is the source of truth

11. Use default import to import React

Don’t:

Do:

To support recommended behaviour you need to set following config within your tsconfig.json file:

{
"compilerOptions": {
/*
Enables emit interoperability between CommonJS and ES Modules
via creation of namespace objects for all imports.
Implies 'allowSyntheticDefaultImports'.
*/
"esModuleInterop": true
}
}

Consider:

NOTE:

- With this style, syntax sugar for using Fragments 👉 <></> won't work. You need to import them explicitly and use via <Fragment>...</Fragment>.

- I like this approach more as it’s explicit and I can add key whenever I want without introducing "too much" changes while doing refactoring.

If you wanna use the “consider section pattern” in whole project without defining jsx pragma per file, you need to set following config within your tsconfig.json file:

{
"compilerOptions": {
/*
Specify the JSX factory function to use
when targeting 'react' JSX emit,
e.g. 'React.createElement' or 'h'.
*/
"jsxFactory": "createElement"
}
}

Why:

  • It’s confusing to import all contents from react library when you’re not using them.
  • It’s more aligned to “idiomatic JS”
  • You don’t need to import types defined on React namespace like you have to do with Flow as TS support declaration merging 👌
  • The “consider” example is even more explicit what is used within your module and may improve tree-shaking during compile time.

12. Don’t use namespace

Don’t:

Do:

Why:

  • namespace was kinda useful in pre ES2015 modules era. We don't need it anymore.
  • Cannot be used with babel for transpiling 👀

If you really need some kind of namespacing within your module, just use idiomatic JavaScript, like in following example:

13. Don’t use ES2015 module imports when importing types without any run-time code

Don’t:

Do:

NOTE:

If you’re having many duplicate imports, consider to aliasing them to local type 👉 type State = import('./counter').Counter['state']

👉 Beware that if you wanna create local type alias from generic type import, you need to mirror that generic type as well, e.g.: 👉 type ReactElement<T=any> = import('React').ReactElement<T>

Why:

  • Your code is explicit for both human and machine. If you don’t use any run-time code, annotate your code via only via import('path')
  • check this great post from David East to learn more

14. Don’t use camelCase/PascalCase for file names

Don’t:

SkaterBoy.tsxuserAccessHandlers.ts

Do:

skater-boy.tsxuser-access-handlers.ts

Why:

  • readable file names. e.g MyHalfFixedDedupedDirResolver vs my-half-fixed-deduped-dir-resolver 👀
  • no more weird git conflicts when renaming/deleting/adding files on various OS file systems (case-sensitive/insensitive)
  • consistency (I don’t have to think if this file is component or some helper or service. tsx extension tells me that)
  • nicely maps to component implementation name skater-boy.tsx 👉 const SkaterBoy = () => {}

15. Declare types before run-time implementation

Don’t:

Do:

Why:

  • first lines of document clearly state what kind of types are used within current module. Also those types are compile only code
  • run-time and compile time declarations are clearly separated
  • in component user immediately knows what the component “API” looks like without scrolling

NOTE:

If you’re leveraging declaration merging as part of your API, define type after implementation:

16. Don’t use method declaration within interface/type alias

Don’t:

Do:

Why:

17. Don’t use number for indexable type key

Don’t:

Do:

Why:

  • In JavaScript object properties are always typeof string! don't create false type predicates within your apps!
  • Annotating keys with number is OK for arrays (array definition from standard .d.ts lib 👉 [n: number]: T;), although in real life you should rarely come into situation that you wanna define "custom" array implementation

18. Don’t use JSX.Element to annotate function/component return type or children/props

Don’t:

Do:

Why:

  • globals are bad ☝️💥
  • TypeScript supports locally scoped JSX to be able to support various JSX factory types and proper JSX type checking per factory. While current react types use still global JSX namespace, it’s gonna change in the future.
  • explicit types over generalized ones

19. Use type alias instead of interface for declaring Props/State

Don’t:

Do:

Why:

  • consistency/clearness. Let’s say we use tip no.8 (defining state type from implementation). If you would like to use interface with this pattern, you’re out of luck, as that’s not allowed within TypeScript.
// $ExpectError ❌
interface State extends typeof initialState {}
const initialState = {
counter: 0
}
  • interface cannot be extended by types created via union or intersection, so you would need to refactor your State/Props interface to type alias in that case.
  • interfaces can be extended globally via declaration merging, if you wanna provide that kind of capabilities to your users you’re doing it wrong (exposing “private” API)

20. Don’t use FunctionComponent<P>/FC<P> to define a function component

Don’t:

Do:

Why:

  • consistency/simplicity (always prefer familiar vanilla JavaScript patterns without too much type noise/magic)
  • FC defines optional children on props, which is not what your API may support as explained in tip no 8. API should be explicit!
  • FC breaks defaultProps type resolution (introduced in TS 3.1) and unfortunately all other "static" props as well 👉(propTypes,contextTypes,displayName)

How FC breaks defaultProps and friends ?

type Props = {
who: string
} & typeof defaultProps

const defaultProps = {
greeting: 'Hello',
}

const Greeter: FC<Props> = (props) => (
<div>
{props.greeting} {props.who}!
</div>
)

// 🚨 This won't work.
// Greeter components API will not mark `greeting` as optional
Greeter.defaultProps = defaultProps

const Test = () => (
<>
{/**
ExpectError ❌
Property 'greeting' is missing
*/}
<Greeter who="Martin" />
</>
)

To fix this you would have to re-define default props, which makes your code a mess… 🤒 Look for yourself! 👉

const Greeter: FC<Props> & { defaultProps: typeof defaultProps } = (
props
) => {
/*...*/
}
  • FC cannot be used to define a generic component

while we can define generic functional component(because it’s just a function):

type Props<T extends object> = {
data: T
when: Date
}

const GenericComponent = <T extends object>(props: Props<T>) => {
return (
<div>
At {props.when} : {JSON.stringify(props.data)}
</div>
)
}

following won’t work and we’ll get compiler error:

type Props<T extends object> = {
data: T
when: Date
}
// $ExpectError ❌
// 🚨 FC cannot set generic Props type.
// We got TS error as T (generic), cannot be possibly defined/inferred
const GenericComponent: FC<Props<T extends object>> = (props) => {
return (
<div>
At {props.when} : {JSON.stringify(props.data)}
</div>
)
}

Summary

That’s it for today! Hope you gonna apply those patterns sooner than later within your code base or even better use them as part of your project style guide. If you do please lemme know how it goes ! 😎

And remember. ☝ Respect, is everything! 😅

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! 🖖 🌊 🏄

--

--

Martin Hochel

Principal Engineer | Google Dev Expert/Microsoft MVP | @ngPartyCz founder | Speaker | Trainer. I 🛹, 🏄‍♂️, 🏂, wake, ⚾️& #oss #js #ts