Building Todo App

This is how it’s gonna look like:

As we see, we’re gonna build following component tree in traditional React one way data flow architecture:

|- <App/>

| |- <CreateTodo/>

| |- <TodoItem/>

| |- <DebugMode/>

| | |- <Debug/>

CreateTodo

Let’s build CreateTodo component, which is gonna be responsible for gathering Todo Item text content:

We’ll start with traditional React Component boilerplate and also we’ll add // @ts-check pragma. What we'll get are compiler errors again...

Why do we get those errors?

Well, TypeScript doesn’t know what’s the type of ev argument. TS infers the type to any in this case, which leads to an compile error in strict mode (not using strict mode is like cheating on your girlfriend, you don't wanna do that right?).

Let’s add some standard JSDoc function param annotations to our two functions to make TS compiler happy:

With that additions, we got rid of the type errors! All that was needed was to Leverage existing standard JSDoc annotations and React types to make our code type-safe. I like this!

Now let’s update our state on input change…

Hmm we got no intellisense, nor any errors 😢… Why?

Well Component is a generic class (which has 2 optional positional generic argument types -> Props and State -> Component<Props,State> ).

We need to create type alias for Props and State and annotate our class again via standard JSDoc:

defining types for Props and State via JSDoc annotations and applying them on generic Component class again via JSDoc @extends keyword

Wow that was easy and our DX was extensively improved in comparison with “just vanilla JS” right?

Now let’s explain briefly all the new code, that we’ve just introduced:

// @ts-check

import React, { Component } from 'react' // 👉1. We just used standard JSDoc to create a type alias with name `Props`, which has type 'object' ( this is standard type within TypeScript / you can also use old JSDoc `Object` type) /**

* @typedef {object} Props

*/

// 👉2. Here we're leveraging TypeScript inference, by using runtime information to create compile time type, so there is only one source of truth! THE IMPLEMENTATION 👌.

// With that said, We create `State` type alias which will get inferred to type `{ readonly description: string }`

// 👉 Readonly ?

// That's because we made initialState immutable via `Object.freeze` and because TS is smart, it inferred it correctly. /**

* @typedef {typeof initialState} State

*/ const initialState = Object.freeze({

description: '',

}) // 👉3. Because we cannot use explicit Generic type annotations within vanilla JS, we have to use JSDoc `@extends` pragma which can consume TypeScript type, even generic.

// Also note, that classes are types within TS so they can be used for annotations.

// With this our CreateTodo component has now strictly typed `this.state`, `this.setState()` and `this.props` thanks to TypeScript. No more typos and runtime errors 💎 /**

* @extends {Component<Props, State>}

*/

export class CreateTodo extends Component { // 👉4. NOTE:

// we are setting state via class property, not within a constructor.

// 🙇‍ PRO TIP:

// You should never use constructor when defining React Component via class, as it introduces unnecessary boilerplate and any logic that you may introduce within it should be extracted to pure function which can be then leveraged to setup particular class property again, via class property 👍 state = initialState }

Now let’s implement our change and submit handlers within our class:

// @ts-check /**

* @extends {Component<Props, State>}

*/

export class CreateTodo extends Component {

/**

* @param {import('react').FormEvent} ev

*/

handleSubmit = (ev) => {

// prevent standard page refresh on submit

ev.preventDefault()

// $ExpectType string

const { description } = this.state // @TODO emit description up // we are setting state back to initial

this.setState(() => initialState)

} /**

* @param {import('react').ChangeEvent<HTMLInputElement>} ev

*/

handleChange = (ev) => {

//

// $ExpectType string

const { value } = ev.target // update internal state as we type

this.setState(() => ({ description: value }))

}

}

With that implemented, we’re missing one final piece of our CreateTodo Component.

We need to define Public API of our component (👉 in React Public API === Component Props).

Vanilla React uses PropTypes for "typing" props of a component, which are validated during runtime. This is indeed "better than nothing", but it introduces runtime overhead which we don't want. Thanks to TypeScript, we can define Props within our JSDoc and with that we will get compile time validation and top notch DX when using our component. All we need is just to update our Props typedef to following:

// 👉 1. we are defining object (props in React are always an object),

// which consist of one property:

// 👉 a callback function which has type of a function, that has one argument

// of type string and returns nothing. That's why we use `void` as a return type. /**

* @typedef {{onCreate: (description:string)=>void}} Props

*/

Now we can finish implementation of handleSubmit :

calling onCreate type safe props callback within handleSubmit

Congratulations ! 🍻

We just implemented 100% type safe React Component with vanilla JS with TS type checking in the background.

NOTE: Whole JSX within render is type-safe as well, try to do a typo in it and TS will yell at you immediately 🐿

Let’s use our CreateTodo within root App component and behold that beautiful tooling ( auto imports ) and API intellisense, with proper type inference. Life's good I'm telling ya 🤩...

Render CreateTodo within App component

Defining Todo Model

Before we continue to build our remaining app components, let’s not forget to implement a very important part of our app 👉 Todo Model.

Let’s create models.js file with //@ts-check pragma on top again.

touch src/app/models.js

Now do you remember that classes are also types within TS type checker? Let’s leverage that knowledge/TS feature:

app/models.js

our Todo model class

With that if we create new instance of our Todo, it’s gonna be an object of type {id:string, done: boolean, description: string} , and because it's an type as well we can reference it within JSDoc annotation, which we'll exactly do in next step while implementing TodoItem component.

TodoItem

We already know all steps/techniques needed to write strictly typed React component within vanilla JS. So this is how TodoItem is gonna look like:

src/app/todo-item.jsx