フロントエンドの保守性を上げるTypeScript実践テクニックを紹介!TypeScriptの型システム・応用方法を解説します

フロントエンドの保守性を上げるTypeScript実践テクニックを紹介!TypeScriptの型システム・応用方法を解説します

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

こんにちは!

近年ではフロントエンドの開発において TypeScript の導入はほぼ必須になってきております。

TypeScript での型システムについて理解しておくことで、フロントエンド開発で適切に型について扱う事ができ、より堅牢なサービス開発を行う事ができます。
複雑になりやすいフロントエンドのチーム開発では大変魅力的ですね。

今回は TypeScript の型推論や型アサーションなどの型システム・応用方法について解説いたします!

はじめに

TypeScript はコンパイルすると JavaScript になるため、
最終的なソースコードは JavaScript をサポートする環境で実行できます。

では、なぜ TypeScript で書く必要があるのかと疑問に思う人がいるかもしれません。
本記事では、フロントエンド の開発における TypeScript の有用性などについても解説いたします。

想定する読者

  • TypeScript の型推論や型アサーションなどについて理解したいヒト
  • TypeScript でサービス開発をした事がないヒト

なぜフロントエンドの開発で TypeScript を使うのか

フロントエンドの開発において、柔軟性よりも堅牢性を選ぶ開発者が増えている事が
TypeScript を使う要因の一つだと思います。

TypeScript を導入する主なメリットは以下の通りです。

  1. API レスポンスへの型付によるタイプセーフなオブジェクト参照
  2. amplify codegen による Graphql レスポンスへの強力な型付
  3. amplify codegen による Mutation, Query リクエストへの強力な型付
  4. Store への安全でタイプセーフなアクセス
  5. オープンソースモジュール(npm)への型付
  6. NuxtJS/VueJS、NextJS/ReactJS コンパイル時の型エラー検出
  7. エディタの自動補完(変数参照、関数参照等々)

詳しくはこちらの記事にまとめておりますのでぜひご覧ください!

また、ユニバーサル JS 構成(バックエンド(例えばLambda)も NodeJS で開発)の場合には
バックエンドとフロントエンドそれぞれで共通の型を利用することが可能となります。

以降は TypeScript の型システムについて詳しく解説いたします。

TypeScript の型システム

TypeScript の型システムは非常に優秀です。
今回は基本的な型システムの機能である以下について解説したいと思います!

  • 型推論
  • 型アサーション
  • 型の互換性

型推論

TypeScript では変数を値で初期化する際に、
必ずしも変数に型を指定する必要はありません。

TypeScript のコンパイラが変数の宣言部分や変数に代入する値の型などを見て、
型を推測(推論)します。

このように初期値から型を推測(推論)することを型推論と呼びます。

let num = 100 // let num: number = 100
console.log(num) // 100

let hasText = false // let hasText: boolean = false
console.log(hasText) // false

以下の動画のように、変数名にカーソルを当てることで 型情報を確認することができます。

カーソルを当てることで型情報が確認できる

関数の引数には Lint でも指定する事が多いため、型情報は付与することが望ましいです。

返り値にも型推論は効くため型情報を付与しなくても良いですが、
基本的にはコードの読みやすさの点からなるべく型の情報はつけた方が良いです。

/**
 * カウントを1増やす
 * @param count 現在のカウント数
 * @returns +1されたカウント数
 */
function increment(count: number) {
  return count++
}
increment(1)
関数の返り値の型を指定しなくてもカーソルを当てることで型情報が確認できる

非同期処理を行うための Promise も型推論する事ができます。
以下のような、引数の数値分処理を遅らせる事ができる関数を例にしてみます。

function waiting(duration: number) {
  return new Promise((resolve) => {
    setTimeout(() => resolve(`${duration}ms passed`), duration);
  });
}
waiting(3000).then((result) => {
  console.log(result) // 3000ms passed
})

Promise の中で setTimeout 関数を定義し、引数の数値分経過した後に
resolve を実行しテキストを返しています。

ただ上記のままだと、以下のような型推論となり、
result の型情報は string ではなくunknown になってしまいます。

function waiting(duration: number): Promise<unknown>

// resultにカーソルを当てた場合
(parameter) result: unknown

正しく型推論を行うには2つの方法があります。

// 1. 関数の返り値に型アノテーションを指定
function waiting(duration: number): Promise<string> {
  return new Promise((resolve) => {
    setTimeout(() => resolve(`${duration}ms passed`), duration);
  });
}
// 2. Promiseのインスタンスを作成した際に型を指定
function waiting(duration: number) {
  return new Promise<string>((resolve) => {
    setTimeout(() => resolve(`${duration}ms passed`), duration);
  });
}

waiting(3000).then((result) => {
  console.log(result) // 3000ms passed
})

このように指定する事で、両者ともに以下のような型推論が効くようになります。

function waiting(duration: number): Promise<string>

// resultにカーソルを当てた場合
(parameter) result: string

また、 async / await を使った場合でも同様な型推論が行われます。

async function waiting(duration: number) {
  return await new Promise<string>((resolve) => {
    setTimeout(() => resolve(`${duration}ms passed`), duration);
  })
}
async/awaitを利用したPromiseの型推論

型アサーション

上述した型推論によって得た型情報は型アサーションと呼ばれる方法で上書きすることができます。

const posts = {}
posts.title = 'hoge'

上記のようにオブジェクトにプロパティを追加しようとすると
以下のようなエラー文が表示されます。

プロパティ 'title' は型 '{}' に存在しません。

これは posts オブジェクトが空のオブジェクトと型推論されるためコンパイルエラーが発生されます。

こちらを解決するための手段として as<> を利用して型アサーションを利用します。

interface IPosts {
  title: string
  content: string
}

// 1. as構文
const posts = {} as IPosts
posts.title = 'hoge'
// こちらはcontentがstringと定義されているためエラー
posts.content = 111

// 2. <>構文
const posts = <IPosts> {}
posts.title = 'hoge'
posts.content = 111
型アサーションの例

<> 構文についてですが、JSX で利用する場合にはタグと混同される恐れがあるため注意が必要です。

const foo = <string>bar;
</string>

もう少し詳細な実用例について説明します。

Vue のバージョン2で開発している際、 コンポーネント間で propsを利用して
Array や Object でのデータを受け渡しをする際は型の情報量が不足しがちです。

以下のように PropType を vue ライブラリからインポートし、
as を利用して型情報を付与します。

import Vue, { PropType } from 'vue'
// ※interfaceなどの型定義は本来は別ファイルで管理しインポートするのが望ましい
interface IPost {
  title: string
  content: string
}
export default Vue.extend({
  props: {
    posts: {
      type: Array as PropType<IPost[]>,
      default: () => [],
      required: true,
    },
  },
})

このように型情報が不足する場合は型アサーションをする事で適切な型を指定する事ができます。

型アサーションは他のプログラミング言語で呼ばれるキャストと同じ意味だと
思われるかもしれませんが、意味が多少異なります。

キャストは一般的に何らかのランタイムサポートを意味します。

一方型アサーションはコンパイル時にコードをどのように解析するか、
型のヒントを提供する方法になってます。

型の互換性

TypeScript の型チェックは構造的部分型に基づいて行われます。
型の互換性を意識したコーディングはコードの意図を表すための重要な観点です。

let hasSample: boolean = false
let number1: number = 0

// NG
// 型 'number' を型 'boolean' に割り当てることはできません
hasSample = number1

上記の例のように、 booleannumber には互換性がないのでコンパイルエラーになります。

any 型や unknown 型の互換性については注意が必要です。

any 型はどのような型にも宣言・代入する事ができます。
下記の例のようにコンパイルエラーが表示されずに
様々な型と互換性をもってしまうため危険な型ではあります。

let hasSample: any = false
let number1: number = 0

// OK
hasSample = number1

unknown 型は型の中でも最も抽象的な型となります。
unknown 型は型アサーションなどによって型が上書きされない限り別の型へ代入することはできません。

let hasSample: unknown = false

// NG
// 型 'unknown' を型 'boolean' に割り当てることはできません
let ok: boolean = hasSample
// OK
let text: string = hasSample as string

上記の例のように、 text は型アサーションにより型情報が付与されコンパイルエラーはなくなります。
ですが、変数名と意味が異なる型になっているため型アサーションでの
型情報の付与は実装者に委ねられるので注意が必要です。

関数の互換性

関数の互換性のチェックは引数の型情報によって判断されます。

let fn1 = function(isSample: boolean, text: string) {}
let fn2 = function(isSample: boolean, hoge: string) {}

// OK
fn1 = fn2
// OK
fn2 = fn1

複数の引数がある場合、引数の名前( fn1fn2 )は互換性に関係なく、
引数の型が一致しているかのチェックが行われます。

以下のように、引数の順番が異なる場合の関数には互換性がないのでコンパイルエラーになります。

let fn1 = function(isSample: boolean, text: string) {}
let fn2 = function(text: string, isSample: boolean) {}

// NG
// 型 '(text: string, isSample: boolean) => void' を
// 型 '(isSample: boolean, text: string) => void' に割り当てることはできません。
fn1 = fn2
fn2 = fn1

また以下のように、引数が多い型への代入は可能ですが、
少ない型への代入はコンパイルエラーになるので注意が必要です。

let fn1 = function(isSample: boolean, text: string) {}
let fn2 = function(isSample: boolean) {}

// OK
fn1 = fn2
// NG
// 型 '(isSample: boolean, text: string) => void' を
// 型 '(isSample: boolean) => void' に割り当てることはできません。
fn2 = fn1

クラス型の互換性

クラス型の互換性のチェックはインスタンスメンバとメソッドが比較対象になります。
静的なメンバーとコンストラクタ関数は比較対象にはなりません。

class Animal {
  feet: number = 4
  constructor(name: string, categoryId: number) {}
}
class Human {
  feet: number = 2
  hands: number = 2
  constructor(name: string, age: number, genderId: number) {}
}
let animal = new Animal('Cat', 1)
let human = new Human('John', 25, 1)

// OK
animal = human
// NG
// プロパティ 'hands' は型 'Animal' にありませんが、型 'Human' では必須です。
human = animal

まとめ

今回は TypeScript の型システム・応用方法についてご紹介しました!

フロントエンドの開発において TypeScript は今後必要不可欠な技術といえるでしょう。

Vue3 と TypeScript の相性の良さについてはこちらの記事で詳しく書いておりますのでぜひご覧ください!

TypeScript でのフロントエンドの開発は、ぜひお気軽にお問い合わせください!