こんにちは!
近年ではフロントエンドの開発において TypeScript の導入はほぼ必須になってきております。
TypeScript での型システムについて理解しておくことで、フロントエンド開発で適切に型について扱う事ができ、より堅牢なサービス開発を行う事ができます。
複雑になりやすいフロントエンドのチーム開発では大変魅力的ですね。
今回は TypeScript の型推論や型アサーションなどの型システム・応用方法について解説いたします!
TypeScript はコンパイルすると JavaScript になるため、
最終的なソースコードは JavaScript をサポートする環境で実行できます。
では、なぜ TypeScript で書く必要があるのかと疑問に思う人がいるかもしれません。
本記事では、フロントエンド の開発における TypeScript の有用性などについても解説いたします。
フロントエンドの開発において、柔軟性よりも堅牢性を選ぶ開発者が増えている事が
TypeScript を使う要因の一つだと思います。
TypeScript を導入する主なメリットは以下の通りです。
詳しくはこちらの記事にまとめておりますのでぜひご覧ください!
また、ユニバーサル JS 構成(バックエンド(例えばLambda)も NodeJS で開発)の場合には
バックエンドとフロントエンドそれぞれで共通の型を利用することが可能となります。
以降は 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);
})
}
上述した型推論によって得た型情報は型アサーションと呼ばれる方法で上書きすることができます。
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
上記の例のように、 boolean
と number
には互換性がないのでコンパイルエラーになります。
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
複数の引数がある場合、引数の名前( fn1
や fn2
)は互換性に関係なく、
引数の型が一致しているかのチェックが行われます。
以下のように、引数の順番が異なる場合の関数には互換性がないのでコンパイルエラーになります。
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 でのフロントエンドの開発は、ぜひお気軽にお問い合わせください!
スモールスタート開発支援、サーバーレス・NoSQLのことなら
ラーゲイトまでご相談ください
低コスト、サーバーレスの
モダナイズ開発をご検討なら
下請け対応可能
Sler企業様からの依頼も歓迎