
今年直すべき10のTypeScriptの悪い習慣
TypeScriptとJavaScriptはここ数年で着実に進化しており、過去数十年で築いた習慣のいくつかは時代遅れになっています。
ここでは、私たちが断ち切るべき10の習慣をご紹介します。
ストリクトモードを使用していない
strict modeを使用していないtsconfig.jsonをはこんな感じになります。
{
"compilerOptions": {
"target": "ES2015",
"module": "commonjs"
}
}
ストリクトモードを有効にしましょう。
{
"compilerOptions": {
"target": "ES2015",
"module": "commonjs",
"strict": true
}
}
ストリクトモードにすべき理由
既存のコードベースに厳格なルールを導入するには時間がかかりますが、ルールを厳しくすると、
将来的にコードの変更が容易になるので、コードの修正に費やした時間を、将来的にリポジトリで作業する際に還元することができます。
||によるデフォルト値の定義
オプション値のために||を使うと、どのように見えるか。
function createBlogPost (text: string, author: string, date?: Date) {
return {
text: text,
author: author,
date: date || new Date()
}
}
あるべき姿
新しい ?? 演算子を使うか、あるいはパラメータレベルでフォールバックを定義するとよいでしょう。
function createBlogPost (text: string, author: string, date: Date = new Date())
return {
text: text,
author: author,
date: date
}
}
?? 演算子は昨年導入されたばかりですが、長い関数の途中で値を使用する場合、パラメータのデフォルトとして設定するのは難しいかもしれません。
また?? は || とは異なり、NULL や undefined に対してのみフォールバックするものであり、すべてのファルシーな値に対してではありません。
タイプとしてのanyの使用
構造がよくわからないときにデータにanyを使うのは避けましょう。
async function loadProducts(): Promise<Product[]> {
const response = await fetch('https://api.mysite.com/products')
const products: any = await response.json()
return products
}
あるべき姿
ほとんどすべての場面で、何かをanyと入力する場合は、代わりにunknownと入力してください。
async function loadProducts(): Promise<Product[]> {
const response = await fetch('https://api.mysite.com/products')
const products: unknown = await response.json()
return products as Product[]
}
なぜそれをするのか
anyは、基本的にすべての型チェックを無効にするので便利です。
公式の型付けでもanyが使われることがよくあります(例えば、上の例のresponse.json()は、TypeScriptチームではPromise<any>として型付けされています)。
なぜいけないのか
基本的にすべての型付けを無効にするからです。
anyを介して入ってきたものは、型チェックを完全に放棄してしまいます。
型構造に関する仮定が実行時のコードに関連している場合にのみコードが失敗するため、バグの発見が困難になります。
val as SomeType
async function loadProducts(): Promise<Product[]> {
const response = await fetch('https://api.mysite.com/products')
const products: unknown = await response.json()
return products as Product[]
}
あるべき姿
function isArrayOfProducts (obj: unknown): obj is Product[] {
return Array.isArray(obj) && obj.every(isProduct)
}
function isProduct (obj: unknown): obj is Product {
return obj != null
&& typeof (obj as Product).id === 'string'
}
async function loadProducts(): Promise<Product[]> {
const response = await fetch('https://api.mysite.com/products')
const products: unknown = await response.json()
if (!isArrayOfProducts(products)) {
throw new TypeError('Received malformed products API response')
}
return products
}
こうあるべき理由
JavaScriptからTypeScriptに変換する際、既存のコードベースではTypeScriptコンパイラが自動的に推論できない型を仮定していることがよくあります。
このようなケースでは、SomeOtherTypeとしてクイックを投入することで、tsconfigの設定を緩めることなく変換を高速化することができます。
テストで as any
interface User {
id: string
firstName: string
lastName: string
email: string
}
test('createEmailText returns text that greats the user by first name', () => {
const user: User = {
firstName: 'John'
} as any
expect(createEmailText(user)).toContain(user.firstName)
}
あるべき姿
テストのためにデータをモックする必要がある場合は、モックするものの横にモックロジックを移動し、再利用できるようにします。
interface User {
id: string
firstName: string
lastName: string
email: string
}
class MockUser implements User {
id = 'id'
firstName = 'John'
lastName = 'Doe'
email = 'john@doe.com'
}
test('createEmailText returns text that greats the user by first name', () => {
const user = new MockUser()
expect(createEmailText(user)).toContain(user.firstName)
}
こうあるべき理由
テストカバレッジが十分でないコードベースでテストを書く場合、複雑なビッグデータ構造があることが多いのですが、テスト対象の特定の機能に必要なのはその一部だけです。他のプロパティを気にする必要がないので、短期的には楽です。
避けるべき理由
モックを作成しないでおくと、直近ではプロパティのひとつが変更されたときに、一箇所ではなくすべてのテストで変更しなければならないという事態に陥ります。また、テスト対象のコードが、以前は重要視していなかったプロパティに依存している場合、その機能に関するすべてのテストを更新する必要があります。
オプショナルプロパティ
あるときはある、ないときはないプロパティをオプションとしてマーク
interface Product {
id: string
type: 'digital' | 'physical'
weightInKg?: number
sizeInMb?: number
}
あるべき姿
どのような組み合わせが存在し、どのような組み合わせが存在しないかを明示的にモデル化します。
interface Product {
id: string
type: 'digital' | 'physical'
}
interface DigitalProduct extends Product {
type: 'digital'
sizeInMb: number
}
interface PhysicalProduct extends Product {
type: 'physical'
weightInKg: number
}
その理由
タイプを分割する代わりにプロパティをオプションとしてマークすることは、より簡単で、より少ないコードを生成します。また、構築される製品をより深く理解する必要があり、製品に関する仮定が変わった場合にコードの使用が制限される可能性があります。
なぜいけないのか
型システムの大きな利点は、実行時のチェックをコンパイル時のチェックに置き換えることができることです。より明示的な型付けを行うことで、例えば、すべてのDigitalProductがsizeInMbを持つことを確認するなど、他の方法では気づかなかったかもしれないバグをコンパイル時にチェックすることが可能になります。
一文字のジェネリック
function head<T> (arr: T[]): T | undefined {
return arr[0]
}
完全に記述的な型名をつけるべきです。
function head<Element> (arr: Element[]): Element | undefined {
return arr[0]
}
なぜそうしているのか
この習慣が広まったのは、公式文書でも1文字の名前を使うようになったからだと思います。また、フルネームを書く代わりにTボタンを押す方が、入力が早く、考える必要もありません。
してはいけない理由
汎用型変数は他の変数と同じです。私たちは、IDEが変数の技術的な部分を表示するようになってから、変数の名前に技術的な部分を記述するという考えを捨てました。例えば、const strName = ‘Daniel’の代わりに、const name = ‘Daniel’とだけ書くようになりました。また、1文字の変数名は、その宣言を見ないと意味を理解するのが難しいため、一般的に嫌われています。
非ブールのブールチェック
値を直接if文に渡すことで、その値が定義されているかどうかをチェックします。
function createNewMessagesResponse (countOfNewMessages?: number) {
if (countOfNewMessages) {
return `You have ${countOfNewMessages} new messages`
}
return 'Error: Could not retrieve number of new messages'
}
あるべき姿
気になる条件を明示的にチェックします。
function createNewMessagesResponse (countOfNewMessages?: number) {
if (countOfNewMessages !== undefined) {
return `You have ${countOfNewMessages} new messages`
}
return 'Error: Could not retrieve number of new messages'
}
チェックする理由
チェック項目を短く書くことで、より簡潔に見え、実際にチェックしたい項目について考えずに済む。
そうしない理由
実際に何をチェックしたいのかを考えるべきかもしれません。例えば、上記の例では、countOfNewMessagesが0の場合の処理が異なります。
バンバンジーのオペレーター
見た目の特徴
非真偽値を真偽値に変換する。
function createNewMessagesResponse (countOfNewMessages?: number) {
if (!!countOfNewMessages) {
return `You have ${countOfNewMessages} new messages`
}
return 'Error: Could not retrieve number of new messages'
}
あるべき姿
気になる条件を明示的にチェックします。
function createNewMessagesResponse (countOfNewMessages?: number) {
if (countOfNewMessages !== undefined) {
return `You have ${countOfNewMessages} new messages`
}
return 'Error: Could not retrieve number of new messages'
}
なぜ私たちはそれをするのか
!!!を理解することは、JavaScriptの世界に入るための儀式のようなものだと思う人もいるでしょう。短くて簡潔に見えますし、すでに慣れている人なら何のことかわかるでしょう。これは、あらゆる値をブール値に変換するためのショートカットです。特に、コードベースにおいて、null、undefined、”などのファルシーな値の間に明確な意味上の区切りがない場合には、この方法が有効です。
なぜいけないのか
多くのショートカットや入会の儀式のように、!!!を使うことは、内部の知識を促進することでコードの真の意味を不明瞭にします。これにより、コードベースは新しい開発者(一般的な開発者であれ、JavaScriptの初心者であれ)にとってアクセスしにくくなります。また、微妙なバグを引き起こすのも簡単です。Non-boolean boolean checks」のcountOfNewMessagesが0になってしまう問題は、「!」で持続します。
! = null
これはどのようなものか
バンバン演算子の妹分である != null は、null と undefined を同時にチェックすることができます。
function createNewMessagesResponse (countOfNewMessages?: number) {
if (countOfNewMessages != null) {
return `You have ${countOfNewMessages} new messages`
}
return 'Error: Could not retrieve number of new messages'
}
あるべき姿
気になる条件を明示的にチェックします。
function createNewMessagesResponse (countOfNewMessages?: number) {
if (countOfNewMessages !== undefined) {
return `You have ${countOfNewMessages} new messages`
}
return 'Error: Could not retrieve number of new messages'
}
なぜそれをするのか
ここまで来れば、あなたのコードベースとあなたのスキルはすでにかなり良い状態にあります。!=よりも !==を使うことを強制しているほとんどのリンティングルールセットでも、 != nullについては除外されています。コードベースにnullとundefinedの明確な区別がない場合、 != nullは両方の可能性に対するチェックを短縮するのに役立ちます。
避けるべき理由
JavaScriptの初期にはnull値は非常に厄介なものでしたが、ストリクトモードのTypeScriptでは、null値は言語のツールベルトの貴重な一員となります。例えば、user.firstName === nullは、文字通りユーザーにファーストネームがないことを意味し、user.firstName === undefinedは、まだそのユーザーに質問していないことを意味します(user.firstName === ”は、ファーストネームが文字通り”であることを意味しますが、実際にどのような名前が存在するかは驚くべきことです)。