TypeScript has been increasingly popular. The typing system helps eliminate most of the coding errors at compile time. Generally, TypeScript costs you a significant amount of time on coding and typing, but later on i will save you much more time on debugging.
This series attempts to facilitate the painful efforts of typing, especially when you are not quite familiar with TypeScript. Thus we will focus on typing and nothing else – anything not specifically related to TypeScript, such as the usage of redux, will be ignored.
The demo project for this series can be downloaded at my GitHub: https://github.com/charlee/todolist.
Create a Project
The popular create-react-app
tool now supports creating application with TypeScript directly.
$ create-react-app todolist --typescript $ cd todolist
Open the project folder and you will see index.tsx
and App.tsx
files. The naming convension is that files originally using JSX end with .tsx
and pure TypeScript files end with .ts
.
You may notice that TypeScript complains about the export of React has an implicit any
type. This is because original React does not contain any typing definitions. Fix this by installing @types/react
:
$ npm install --save @types/react
I would also recommend adding a .prettierrc.yaml
in the project root directory with the following content. If you are using Visual Studio Code, you can press Cmd+Shift+F (Mac) or Ctrl+Shift+I (Windows/Linux) to auto format files:
printWidth: 90 tabWidth: 2 singleQuote: true trailingComma: all
Component
It is also useful to define all the data models in one place, namely, src/models/types.d.ts
:
declare module 'Models' { export interface Todo { id: number; text: string; done: boolean; }; export type TodoList = Todo[]; }
When creating a component, you need to create an interface type for the props:
import React, { useState } from 'react'; import { Todo } from 'Models'; export interface IProps { todo: Todo; } const Todo: React.FC<Todo> = props => { const { todo } = props; const [expanded, setExpanded] = useState<boolean>(false); return <div>{todo.text}</div>; }; export default Todo;
Or if you need to define old style components with classes, you can do this:
import React from 'react'; import { Todo } from 'Models'; export interface IProps { todo: Todo; } export interface IState { expanded: boolean; } class Todo extends React.Component<IProps, IState> { state: IState = { expanded: false, }; render() { const { todo } = this.props; return <div>{todo.text}</div>; } } export default Todo;
Note that in the class definition, we need to define the type of the state
property explicitly. This is because TypeScript cannot infer its type from the class (namely React.Component<IProps, IState>
). Omitting the type like state = { expanded: false }
works for this simple case, but if the state contains any typed list, TypeScript will complain that the empty list does not fit the type of the list:
// State type definition export interface IState { todos: Todo[]; } class TodoList extends React.Component<IProps, IState> { state = { todos: [] }; // WRONG: [] does not match Todo[] }
Generic Component
One nice thing of TypeScript is generic. It allows us to extract the algorithm from the code. Similarly we can create generic component as well, to handle some common UI behaviors.
The following example shows a generic component called FilteredList
. It accepts a list of objects in arbitrary type, a filter function, and filters the object list with the filter function, then pass the filtered list to its children.
import React from 'react'; export interface IProps<T> { objects: T[]; filter: (o: T) => boolean; children: (objects: T[]) => JSX.Element; } const FilteredList = <T extends {}>(props: IProps<T>) => { const { children, objects, filter } = props; const filtered = objects.filter(o => filter(o)); return <React.Fragment>{children(filtered)}</React.Fragment>; }; export default FilteredList;
In above code we used a trick <T extends {}>
instead of <T>
. Although <T>
is legal in TypeScript, it would be recognized as a tag in JSX and produce a compile error.
Note that although FilteredList
is generic, in some case it can be used directly, given that TypeScript can infer its type parameter from its attributes:
export interface Todo { id: number; name: string; } export interface IProps { todos: Todo[]; } const TodoList: React.FC<IProps> = props => { const { todos } = props; return ( <FilteredList objects={todos} filter={o => o.name.startsWith('[URGENT]')}> {todos => ( <React.Fragment> {todos.map(todo => ( <TodoItem todo={todo} /> ))} </React.Fragment> )} </FilteredList> ); };
Here we did not specify what T
is when using <FilteredList>
. It is completely inferred from its objects
attribute.
That’s all for this post. I know I didn’t cover everything about typing, so you are welcome to comment anything you think worth to mention here. Thanks for reading!
Hello, thanks for article. After text `When creating a component, you need to create an interface type for the props:` you have mistake in React.FC. React.Fc is deprecated, use React.FuctionalComponent 🙂
Thanks for pointing that out. I double checked `@types/react` and figured out that `React.FC` is not deprecated.
https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/react/index.d.ts#L502
The deprecated one is `React.SFC` since function component is no longer stateless. React.FC is simply an alias for React.FunctionComponent.