TypeScript コーディングレギュレーションを紹介します😎

TypeScript コーディングレギュレーションを紹介します😎

こんにちは!

私たちが最も得意とする言語は TypeScript です。TypeScript はフロントエンドの開発時はもちろんですがバックエンドの NodeJS へのビルドも可能なため、同じ言語をフロントエンドとバックエンドを扱えるため大変重宝しています。(言語が統一化され、エンジニアリソースの確保がしやすく、加えて社内エンジニアの育成もしやすい)

バックエンドの言語として時折 Python も採用することがありますが、機械学習や LLM 開発時などユースケースは限定的です。

本記事では、普段私たちが採用している内部向けのコーディングレギュレーションの一部を紹介します!参考にし、自社のレギュレーションに取り込んでみてください。

想定する読者

  • TypeScript で実装を行なっているヒト
  • TypeScript の実装にチーム内での共通意識を持ちたいヒト
  • TypeScript を採用しようか悩んでいるヒト

はじめに

なぜ TypeScript

まず TypeScript はコーディング中及びビルド時に型エラーを検出し、デプロイ後のランタイムエラーの可能性を減らします。従来の JavaScript の実装では、結合試験時にバックエンドのランタイム上で存在しないプロパティを参照してエラーになったり、タイポでエラーになったりと、実行時にしか検出できない不具合が多々ありました。(単体テストでカバーするという思考もありますが)

TypeScript はコーディングを型安全(タイプセーフ)に行えるため、不具合軽減はもちろんのこと、エンジニアの実装中のストレス軽減にも繋がります。以下は少々極端な例ですが従来のコードと TypeScript の例です。

// JavaScript
const foo = {
  hoge: 1
}

console.log(foo.hoge); // 1
console.log(foo.fuga); // undefined
console.log(huga.foo); // 実行時にエラー
console.log(foo.hoge.fuga); // 実行時にエラー
// TypeScript
type Foo = {
  hoge: number
}
const foo:Foo = {
  hoge: 1
}

console.log(foo.hoge); // 1
console.log(foo.fuga); // コーディング中にエラー検出可
console.log(huga.foo); // コーディング中にエラー検出可
console.log(foo.hoge.fuga); // コーディング中にエラー検出可

TypeScript を使用するユースケース

以下のようなユースケースで TypeScript を使用します。

  • Nuxt3 / Vue3
  • Next / React
  • AWS Lambda(NodeJS)
  • AWS ECS(Docker, NodeJS)

TypeScript コーディングレギュレーション

TypeScript Interfaces vs Classes: When to Use Each One | by Nic Chong |  Geek Culture | Medium

基本ルール

遵守事項補足
utils/*.ts は 外部サービスでも利用できるレベルの汎用的機能をまとめる判断が難しい、または不明な場合は当社へ確認の上実装を行って下さい。
services/*.ts はプロダクトに依存した機能をまとめる判断が難しい、または不明な場合は当社へ確認の上実装を行って下さい。
ESLint の Warn 及び Error はすべて解消コミット前に ESLint を実行しフォーマットを実行して下さい。
OSS のパッケージの導入は事前承認性OSS パッケージ及び独自の製品の導入については当社へ承認を得てから導入して下さい。
タイプセーフ実装テストコードも TypeScript でタイプセーフに実装して下さい。
ただし、テストコードで意図して例外を発生させる必要がある場合は、タイプセーフな実装は問いません。
コミット作成前にローカルでのテスト疎通を確認テストコードがすでに実装されているプロジェクトの場合、テストが疎通しないコードをあげるのは控えましょう。
テストコードのカバレッジ計測カバレッジは指定がなければC1レベルを目指して実装してください。

関数型プログラミング&イミュータブルプログラミング

イミュータブルプログラミングを行う目的

理由詳細
予測可能性イミュータブルなオブジェクトは一度作成されるとその状態が変更されないため、プログラムの挙動を予測しやすくなります。
副作用の排除データが変更されると、それに依存する他の部分で予期せぬエラーが発生する可能性があります。しかし、イミュータブルなデータは変更が不可能なため、このような副作用を防ぐことができます。
デバッグの容易性データの変更が追跡しやすくなり、バグの発見と修正が容易になります。

総じて、バグの軽減や可読性の向上などメリットが非常に大きいです。 デメリットは通常の慣習的なプログラミングと比べて、実行効率がやや悪くなることが言えますが、現代のハードウェアの性能であればそこまで気にする必要はありません。イミュータブルであっても、慣習的な実装であっても速度に大差はありません。

イミュータブルと関数型プログラミングの関係性

関数型プログラミングは、イミュータブルなデータを前提としています。 関数型プログラミングは、データの変更を行わず、副作用のない関数(汎用的な関数)を使ってメソッドチェーンを使用しデータを変換することを目指します。

実装例 – 生徒の点数の合計を求める

概念的な話のみで理解は難しいと思いますので、以下の実装例を記載します。

慣習的な実装

let totalScore = 0
const students = [
  { name: 'Alice', score: 50 },
  { name: 'Bob', score: 60 },
  { name: 'Charlie', score: 70 },
]
for (let i = 0; i < students.length; i++) {
  totalScore += students[i].score
}
console.log(totalScore) // 180

イミュータブルな実装

import _ from 'lodash'
const students = [
  { name: 'Alice', score: 50 },
  { name: 'Bob', score: 60 },
  { name: 'Charlie', score: 70 },
]
const totalScore = _.sumBy(students, 'score')
console.log(totalScore) // 180

Lodash に目を通しておく

The beautiful world of lodash. Because we can never have enough lodash… |  by Miguel Oliveira | Medium

Lodashから提供されるその他の関数に目を通しておいて下さい。 以下は頻度多く使用する関数の例です。 特に chainを用いたメソッドチェーンを使用することで、変数を宣言しない関数型なプログラミングを実現できます。

  • chain
  • filter
  • map
  • reduce
  • find
  • findIndex
  • every
  • some
  • includes
  • uniq
  • get
  • head
  • turu
  • tap
  • sortBy
  • groupBy
  • orderBy
  • isEmpty
  • isUndefined
  • isNull
  • isString
  • isNumber

const のみを使用

letvar を使用しないようにしてください。 関数型とイミュータブル意識があれば、varlet 使用せずにほとんどのケースを実装可能です。

変数と関数

変数と関数名には camelCase を使用

悪い

const FooVar
function BarFunc() {}
const BarFunc = () => {}

良い

const fooVar
function barFunc() {}
const barFunc = () => {}

アロー関数を使用

悪い

function BarFunc() {}

良い

const barFunc = () => {}

クラス

クラス名には PascalCase を使用

悪い

class foo {}

良い

class Foo {}

クラスメンバとメソッドの camelCase を使用

悪い

class Foo {
  Bar: numberBaz() {}
}

良い

class Foo {
  bar: numberbaz() {}
}

インターフェース

名前には PascalCase を使用

加えて、以下も遵守してください。

  • プレフィックスにIを付けない

悪い

interface IFoo {}
interface foo {}

良い

interface Foo {}

メンバには camelCase を使用

悪い

interface Foo {
  Bar: number
}

良い

interface Foo {
  bar: number
}

タイプ

名前には PascalCase を使用

悪い

type foo = {}

良い

type Foo = {}

メンバには camelCase を使用

悪い

type Foo = {
  Bar: number
}

良い

type Foo = {
  bar: number
}

Enum

enum名にはPascalCaseを使用

悪い

enum color {}

良い

enum Color {}

enum メンバに PascalCase を使用

悪い

enum Color {
  red = 'red',
}

良い

enum Color {
  Red = 'red',
}

null vs undefined

明示的に使用不可能にするために、どちらも使用ししない

悪い

const foo = { x: 123, y: undefined }

良い

const foo: { x: number; y?: number } = { x: 123 }

一般的に undefined を使用(代わりに{valid:boolean,value?:Foo}のようなオブジェクトを返すことを検討)

悪い

return null

良い

return undefined

比較演算子

原則 Lodash の使用を検討

悪い

const foo = ''
if (foo === '')
if (foo === undefined)
if (foo === null)

良い

import _ from 'lodash'

const foo = ''
if(_.isString(foo) && _.isEmpty(foo))
if(_.isUndefined(foo))
if(_.isNull(foo))

※ isEmpty は数値の0を true 判定するため、注意してください。

Lodash について詳しくはこちら

== vs ===

===を使用してください。

悪い

if (foo == 0)

良い

if (foo === 0)

フォーマット

Prettier

prettierを使用して、ファイル保存時及びコミット前にフォーマットが自動実行されます。 prettierの修正に従ってください。

type vs interface

ユニオン型や交差型が必要な場合には type を使用
type Foo = number | { someProperty: number }
extend や implements をしたいときは interface を使用
interface Foo {
  foo: string
}
interface FooBar extends Foo {
  bar: string
}
class X implements FooBar {
  foo: stringbar: string
}

フロントエンド開発でオブジェクト指向な実装はあまりしない想定しないため、迷ったらとりあえず type を使ってください。 逆に共通処理の実装などを行う場合は、interface の使用を検討してください。

タイプセーフ

タイプセーフな変数を定義

悪い

type Foo = {
  bar: string
}
const foo = {} as Foo // 型エラーを検出できない

良い

type Foo = {
  bar: string
}
const foo: Foo = {} // 型エラーを検出できる

まとめ

本記事では私たちの TypeScript のレギュレーションを一部紹介しました。お好みでプロジェクトに合わせてカスタマイズしてみてください。

AWSサーバーレスフロントエンドの開発はお気軽にお問い合わせください。