(※画像はGeekbotさんの記事より引用)
ベトナム出身のフルスタックエンジニアNguyen(グエン)さんが、ReactにおけるJavaScriptとTypeScriptについての記事を書いてくれました!
Zealsでは、フロントエンド開発にReactフレームワークを導入しています。React環境には他にもJestなどのテスト環境や、TypeScrptの導入なども先行して行っています。
しかしTypeScriptについては、これまでは主にJavaScriptでフロントエンドの開発を進めてきたため、現在は両方の開発言語が混在している状態にあります。
そのような環境で、実際にフロントエンド開発に携わるエンジニアがどう考えて実装を進めているかについて、Nguyuenさんが記事を書いてくれました。ぜひ色んな方に読んでいただきたいので、後半では文章を日本語訳して書いております。英語が苦手な方はそちらをお読み頂けると嬉しいです!
それでは本編、お楽しみください。
- Should you convert your React App from Javascript to Typescript?
- Why we decided to integrate Typescript
- Let's convert some existing components from JS to TS and see the magic
- Conclusion
- References
- [日本語訳]なぜReactをJSからTypeScriptに変換すべきなのか?
- Typescriptを既存のReactに統合した理由
- 既存のJSコンポーネントをTSに変換する方法
- 結論
- リファレンス
- さいごに(訳者あとがき)
Should you convert your React App from Javascript to Typescript?
Hi everyone, my name is Nguyen and I'm a front end engineer at ZEALS. Before, I only used Javascript and React to develop. But since I've learned Typescript and have enjoyed writing in TypeScript for more than a few months, now I feel I will struggle without it! Currently Angular has wonderful Typescript support, but how about Typescript with React? What are the advantages and disadvantages of doing so? I would like to share my throughts on why should we chose to integrate Typescript in our large React.
Why we decided to integrate Typescript
- Our team is getting bigger and there are many developers are working together. We think Typescript will help new developers understand our workflow and data structures used throughout the project.
- The front-end team wants to decrease small bugs while coding.
- We think new developers will find it slightly easier to maintain the existing codebase.
- We also think that the code will be easier to refactor in the future.
Potential Issues
- It takes some time to get used to Typescript.
Let's convert some existing components from JS to TS and see the magic
Note
- The following example is React <~ 16.8.0
I have a sample React component made in Javascript. This component has props, state and fetches data through an API.
- The referencing repository are here
https://github.com/knguyen30111/diary
// This is mock API to fetch data from the Database const todos = [ { id: 1, text: "Learn React", done: true, priority: 1 }, { id: 2, text: "Learn JSX", done: false, priority: 3 } ]; export function getTodoList() { return Promise.resolve(todos); } export function getTodo(id) { const todo = todos.find(todo => todo.id === parseInt(id)); return Promise.resolve(todo); }
import React, { Component } from 'react' import PropTypes from 'prop-types // mock fetch API import { getTodo } from './mock-api' class TodoDetailContainer extends Component { state = { id: null, text: '', done: null, priority: null, } componentDidMount() { const { match } = this.props // This is mock fetch API getTodo(match.params.id) .then(todo => this.setState({ ...todo })) .catch(error => console.error(error)) } render() { return ( <div> <pre>{JSON.stringify(this.state, null, 2)}</pre> </div> ) } } TodoDetailContainer.propTypes = { match: PropTypes.object } export default TodoDetailContainer
When you look at both of the files above, it has some potential problems:
1. The writer of this component was not clear about the types in the component
2. I have to guess what the types of the state will be
3. It's not clear what the type of the API call should return
I applied Typescript to both of files above:
const todos: Array<TodoData> = [ { id: 1, text: "Learn React", done: true, priority: 1 }, { id: 2, text: "Learn JSX", done: false, priority: 3 } ]; //This is one object with datatype in array response must have id, text, priority and optional is done export interface TodoData { id: number, text: string, done: boolean, priority: number, } //This is mock API to response Promise with Array TodoData export function getTodoList(): Promise<Array<TodoData>> { return Promise.resolve(todos); } //This is a mock API that responds with todoData export function getTodo(id: string): Promise<TodoData> { const todo = todos.find(todo => todo.id === Number(id)); return Promise.resolve(todo); }
import React, { Component } from 'react' import { TodoData, getTodo } from './mock-api' //Define Props dataType interface Props { match: { params: { id: string } } } type State = TodoData class TodoDetailContainer extends Component<Props, State> { state = { id: 0, text: '', done: false, priority: 0, } componentDidMount() { const { match } = this.props // This is mock fetch API getTodo(match.params.id) // Typescript tells us the result of this function is `Promise<TodoData>`, which ensures that setState is being used correctly .then((todo) => this.setState({ ...todo })) .catch((error) => console.error(error)) } render() { return ( <div> <pre>{JSON.stringify(this.state, null, 2)}</pre> </div> ) } } export default TodoDetailContainer
After converting to Typescript, the component with me is totally understandable:
- First thing I want to talk about Fetch API:
・Through Typescript, I know getTodo(id: string)
needs argument id with datatype is string and response data is Promise<TodoData>
. We don't need to worry about if the API requires the ID as a number
or string
. In addition, we know that we are setting the state of the component correctly with TodoData
.
・Frontend and backend members know the request and response data. This kind of information can be maintained through documentation, but maintaining documentation about many APIs can be difficult. But when you see above, It's clear to all members that the component works correctly, even if they haven't read the API functions module.
・Typescript's static analysis will let us know when we've made a mistake. If the API changes, simply update the interface and Typescript will tell you if you're missing something or setting something incorrectly.
- The second thing to notice is the component is easier to understand:
・There's no need to use the PropType
library! We can simply use Typescript's typing system instead of setting propTypes
on the component.
・And we can use the same type of system for the state as well! We were able to define the components State
as an alias of TodoData
, which means that if someone updates that interface, this component's state will automatically be updated. In addition, the compiler will complain that this component's initial state is not being set correctly if there is a change to that interface and it is lacking something necessary.
If you prefer React Hooks, over class components, I have converted the previous example to a React Hooks version and put it below:
Note
- The following example is React 16.8.0 above
import React, { useState, useEffect } from 'react' import { getTodo } from '../mock-api' const TodoDetailContainer = props => { const { match } = props; const [todo, setTodo] = useState(null); useEffect(() => { const runEffect = async () => { const id = match.params.id try { const todo = await getTodo(id) setTodo(todo) } catch (e) { console.error(e) } } runEffect() }, [match.params.id, setTodo]) return ( <div> <pre>{JSON.stringify(todo, null, 2)}</pre> </div> ) } export default TodoDetailContainer
import React, { useState, useEffect } from 'react' import { TodoData, getTodo } from './mock-api' //Define Props interface interface Props { match: { params: { id: string } } } type State = TodoData const TodoDetailContainer: React.FC<Props> = props => { const { match } = props; const [todo, setTodo] = useState<State>({ id: 0, done: false, priority: 0, text: '' }); useEffect(() => { const runEffect = async () => { const id: string = match.params.id try { const todo = await getTodo(id) setTodo(todo) } catch (e) { console.error(e) } } runEffect() }, [match.params.id, setTodo]) return ( <div> <pre>{JSON.stringify(todo, null, 2)}</pre> </div> ) } export default TodoDetailContainer
Conclusion
After converting JS to TS, I found the experience mostly positive:
- Small bugs are gone while coding (or caught by the compiler).
- The workflow is clear with every single function or component. The types of inputs and outputs of functions/components are defined and communicated to anybody reading the code.
- New members can immediately maintain our codebase if we have TypeScript components.
- In the future, if somebody wants to refactor a component or function. It's easier to do because we know for sure the data inside components and functions. In addition, Typescript will tell us if we've forgot to return something, or carry out an operation that is incompatible with the types.
But that's not to say that it was all positive, there are some potential problems with converting an existing JS codebase to TS:
- I think if the codebase is small, we don't need to apply Typescript. In fact, when applying Typescript to a small codebase you may find that adding types takes more time than is worth the benefit. However, if you are working in a big team with a large codebase, the extra typing will save you hours of hunting down small bugs and increase understanding among your team members.
- I don't recommend you convert all existing components to Typescript. It takes time to do this and you may not receive much benefit. But when creating the new components applying TypeScript will pay dividends.
References
[日本語訳]なぜReactをJSからTypeScriptに変換すべきなのか?
みなさん、こんにちは!グエンです。ZEALSでフロントエンドエンジニアをやっています。
私はこれまで、Zealsのフロントエンド開発でJavascriptしか使ってきませんでした。しかしTypescriptを勉強してからはTypeScriptばかりを使っていて、もはやTypeScriptなしでは生きられない身体になってしまいました。笑
現在、AngularではTypescriptを十分にサポートしていますが、TypeScriptとReactの組み合わせについてはどうでしょうか?大規模なReactにTypescriptを統合することを選択した理由について、メリットやデメリットなど、私の考えを皆さんに共有したいと思います。
Typescriptを既存のReactに統合した理由
- 開発チームが大きくなってきたため。 Typescriptであれば、プロジェクト全体で使用されるワークフローとデータ構造を、新しく入ってきたエンジニアがキャッチアップしやすい 2.フロントエンドチームは、開発中に起こり得るバグを事前に少しでも減らしたい 3.新しく入ってきたエンジニアは、新しいコードベースをイチから作るより既存のコードベースを使う方が簡単なはず 4.TypeScriptコードは将来的にリファクタリングしやすくなる(と考えられる)ため
潜在的な問題
- Typescriptに慣れるには時間がかかる
既存のJSコンポーネントをTSに変換する方法
注意
- 以降では、Reactは16.8.0以下のバージョンの利用を前提としています
Javascriptで作成したサンプルのReactコンポーネントがあります。このコンポーネントには、APIを介してStateとFetchデータを保持しています。
- サンプルリポジトリはこちら
https://github.com/knguyen30111/diary
// This is mock API to fetch data from the Database const todos = [ { id: 1, text: "Learn React", done: true, priority: 1 }, { id: 2, text: "Learn JSX", done: false, priority: 3 } ]; export function getTodoList() { return Promise.resolve(todos); } export function getTodo(id) { const todo = todos.find(todo => todo.id === parseInt(id)); return Promise.resolve(todo); }
import React, { Component } from 'react' import PropTypes from 'prop-types // mock fetch API import { getTodo } from './mock-api' class TodoDetailContainer extends Component { state = { id: null, text: '', done: null, priority: null, } componentDidMount() { const { match } = this.props // This is mock fetch API getTodo(match.params.id) .then(todo => this.setState({ ...todo })) .catch(error => console.error(error)) } render() { return ( <div> <pre>{JSON.stringify(this.state, null, 2)}</pre> </div> ) } } TodoDetailContainer.propTypes = { match: PropTypes.object } export default TodoDetailContainer
上記のファイルには、いくつか問題があります。
- このコンポーネントの作成者は、コンポーネントのTypeを明確に示していない
- コールドリーディングの際、StateのTypeを予測しなければならない
- APIを叩く時に、どのようなTypeが返ってくることを期待しているかがよくわからない
そこで、上記のファイルにTypescriptを適用してみましょう。
const todos: Array<TodoData> = [ { id: 1, text: "Learn React", done: true, priority: 1 }, { id: 2, text: "Learn JSX", done: false, priority: 3 } ]; //This is one object with datatype in array response must have id, text, priority and optional is done export interface TodoData { id: number, text: string, done: boolean, priority: number, } //This is mock API to response Promise with Array TodoData export function getTodoList(): Promise<Array<TodoData>> { return Promise.resolve(todos); } //This is a mock API that responds with todoData export function getTodo(id: string): Promise<TodoData> { const todo = todos.find(todo => todo.id === Number(id)); return Promise.resolve(todo); }
import React, { Component } from 'react' import { TodoData, getTodo } from './mock-api' //Define Props dataType interface Props { match: { params: { id: string } } } type State = TodoData class TodoDetailContainer extends Component<Props, State> { state = { id: 0, text: '', done: false, priority: 0, } componentDidMount() { const { match } = this.props // This is mock fetch API getTodo(match.params.id) // Typescript tells us the result of this function is `Promise<TodoData>`, which ensures that setState is being used correctly .then((todo) => this.setState({ ...todo })) .catch((error) => console.error(error)) } render() { return ( <div> <pre>{JSON.stringify(this.state, null, 2)}</pre> </div> ) } } export default TodoDetailContainer
Typescriptに変換すれば、コンポーネントの中身を完全に理解することができるのではないでしょうか?
- Fetch APIの前提
・ getTodo(id:string)
は引数idがデータ型であり、応答データが Promise <TodoData>
である必要があるとわかります。 IDを「数値」または「文字列」として渡す必要があるかどうかを気にしなくてよくなりますね。さらに、 TodoData
を使用してコンポーネントのStateを正しく設定していることがわかります。
・フロントエンドとバックエンドのエンジニアはリクエストとレスポンスのデータの内容を理解できます。この種のノウハウはドキュメントを残すことで共有できますが、ドキュメントをうまく残して運用することは簡単ではありません。ただし、上記を見ると、API関数モジュールを読み取っていなくても、コンポーネントが正しく機能することを読み取れると思います。
・Typescriptの静的解析は、コードのミスを教えてくれます。 APIが変更された場合はインターフェースを更新するだけで、Typescriptが間違っている内容を知らせてくれます。
- 2番目に注意することは、コンポーネントが理解しやすいことです。
・「PropType」ライブラリを使用する必要はありません!コンポーネントに「propTypes」を設定する代わりに、Typescriptの型システムを使用できます。
・また、Stateにも同じく型システムを使用できます!コンポーネント State
をTodoData
のエイリアスとして定義できました。つまり、誰かがインターフェースを更新すると、このコンポーネントのStateの型が動的に変更されます。さらに、コンパイラはそのインターフェースの変更に間違いがあれば、コンポーネントのStateが正しく設定されていないことを教えてくれます。
クラスコンポーネントよりもReact Hooksを使いたい場合は、前の例をReact Hooksバージョンに変換したものを以下に示します。
注意
- 以降では、Reactは16.8.0以下のバージョンの利用を前提としています
import React, { useState, useEffect } from 'react' import { getTodo } from '../mock-api' const TodoDetailContainer = props => { const { match } = props; const [todo, setTodo] = useState(null); useEffect(() => { const runEffect = async () => { const id = match.params.id try { const todo = await getTodo(id) setTodo(todo) } catch (e) { console.error(e) } } runEffect() }, [match.params.id, setTodo]) return ( <div> <pre>{JSON.stringify(todo, null, 2)}</pre> </div> ) } export default TodoDetailContainer
import React, { useState, useEffect } from 'react' import { TodoData, getTodo } from './mock-api' //Define Props interface interface Props { match: { params: { id: string } } } type State = TodoData const TodoDetailContainer: React.FC<Props> = props => { const { match } = props; const [todo, setTodo] = useState<State>({ id: 0, done: false, priority: 0, text: '' }); useEffect(() => { const runEffect = async () => { const id: string = match.params.id try { const todo = await getTodo(id) setTodo(todo) } catch (e) { console.error(e) } } runEffect() }, [match.params.id, setTodo]) return ( <div> <pre>{JSON.stringify(todo, null, 2)}</pre> </div> ) } export default TodoDetailContainer
結論
JSをTypeScriptに変換するメリットはたくさんあります。
- コーディング中(またはコンパイラーのキャッチ中)に小さなバグがなくなった
- すべての機能またはコンポーネントのワークフローが明確になった。関数/コンポーネントの入力と出力のTypeが定義され、読みやすいコードに
- TypeScriptコンポーネントがある場合、新しく入ってきたエンジニアが既存のコードベースを簡単に使い回すことができます
- コンポーネントと関数内のデータが理解しやすいため、コンポーネントまたは機能をリファクタリングしやすいです
- さらに、Typescriptは返り値を忘れたり互換性のない操作を実行すれば、それを教えてくれます
しかし、デメリットもあると考えています。
- Typescriptを小さなコードベースに適用する場合、型追加の時間がもったいないと感じることがあります(ただし、大規模なコードベースやチームで開発する場合、時間を節約することにつながります)
- 既存のコンポーネントすべてをTypescriptに変換することはお勧めしません。時間がかかる上に、あまりメリットがない場合があります(ただし、新しいコンポーネントを作成する時はTypeScriptを使いましょう)
リファレンス
さいごに(訳者あとがき)
本編は以上です。いかがでしたでしょうか?
TypeScriptやReactを用いたフロントエンド開発に携わりたい方、あるいはグローバルな環境でチャレンジしたい方にとって、Zealsはすごくいい環境を提供できると考えています。
そんなエンジニアの方がいらっしゃれば、ぜひZealsにご応募ください!