記事のターゲット
- JavaScriptをある程度使える、TypeScriptを覚えたい人
記事の目的
- TypeScriptについてザックリ知る。
- TypeScriptを利用するにあたって知っておいた方が良い心得を学ぶ。
- TypeScriptを利用する際によく使うデータ型についての知識を得る。
- TypeScriptに触れてみる。
JavaScriptとTypeScript
JavaScritp(JS)とTypeScript(TS)の違いは、型定義の有無です。
TSは型定義の分だけ、コードに詰め込める情報量が増えます。
情報量が増えると何が嬉しいのかというと、コードを使う際のヒントが増えて使い方が分かりやすくなります。
つまり、TSを使うと以下のようになります。
- デベロッパーは大変になる
- ユーザーは楽になる
このことから、「TypeScriptを使うと楽になるらしい」でTSを使い始めると「話が違う!!」となりがちです。
ここで理解して欲しいのは、プログラミングにおいてデベロッパーとユーザーは表裏一体だということです。いま書いているコードを使うのは、明日の自分かもしれないですし、あるいは同じチームのメンバーかもしれません。
ですから、過去のコードが増えれば増えるほど、TSの恩恵は大きくなります。このあたりが、「TSは大規模開発に向いている」と言われる所以でしょう。
もう一つ、知っておいてもらいたいのが、TSを使ったからと言って必ず良いコードが書けるようになるわけではない、ということです。むしろ型定義が増える分、JS以上にひどいコードを書くこともできます。
短期的な負担が増え、スパゲティコードのせいで長期的な恩恵も受けられないという、踏んだり蹴ったりな状態にもなりかねません。
TSの使う際はユーザー(未来の自分・チーム)にどう使ってもらうか、という視点を常に持って開発していく必要があります。
それが出来ない場合、TSは「妙な枷があるJS」に成り下がってしまうと心得ましょう。
TypeScriptの型注釈
ネガティブなことを言って身構えさせてしまいましたが、JSが使えるならばTSは難しくありません。
とりあえず型注釈のつけ方を知ればTSを使えるようになります。
型注釈とは変数や関数のデータ型を明示することを言います。
変数
変数名の後ろに:
(コロン)を付け、その後にデータ型名を書きます。(指定できるデータ型については、この後でやります。)
例では変数のデータ型にnumber
(数値型)を指定しています。
JSで宣言した変数には数値・文字列・オブジェクトなど何でも代入することができてしまいますが、TSの変数には指定したデータ型しか代入する事ができません。
let value: number;
value = 12; // OK
value = "hoge"; // エラー!
value = {}; // エラー!
関数
関数には、引数と戻り値に型注釈を付けることができます。
引数は変数と同じように引数名の後ろに:
とデータ型を書きます。
戻り値の型は引数の()
の後ろに:
とデータ型を書きます。
function hoge(a: number, b: string): boolean {
// ~~ 省略 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
}
データ型とは何ぞや
さて、TSのデータ型の詳細を説明する前に、そもそもデータ型とは何ぞやという話をしたいと思います。
ググってみると以下のようにあります。
データ型とは、プログラミング言語などが扱うデータをいくつかの種類に分類し、それぞれについて名称や特性、範囲、扱い方、表記法、メモリ上での記録方式などの規約を定めたもの。
こ、小難しい……。 簡単に言ってしまえば、データの種類の事です。
JSやTSはかなり高級な言語で、抽象度が高いためメモリなどを意識することは殆どありません。
しかし、ここら辺を深堀しておくとデータ型に対する理解がグッと深まりますので、ちょっと小難しい話をしておきます。
データの表現
コンピューターは全てのデータを0か1で管理しています。
例えば以下のようなデータです。
1000 0001
この8bitのデータを私たちにわかる値に直すと何になるでしょうか? 例えば整数だと?
2進数1000 0001
を10進数にして129?
なるほど、正解です。
でもちょっと待ってください。
整数には負の値も含まれます。コンピュータは0と1しか扱えないため、マイナス符号が使えません。
では、コンピュータではマイナスの値は扱えないのでしょうか?
そんな事はありませんよね。
例えばですが、先頭の1bitが0なら正の数、1なら負の数とすることで表すことができます。
この表現で1000 0001
を値に翻訳すると、符号部分は1
でマイナス、値部分は000 0001
で1となり結果は**-1**になります。
マイナスの値も表せて一安心ですが、実はまだ問題があります。
先頭1bitで正負を表すだけだと、0が2種類できてしまうのです。(0 と -0)
この点を改善するため、プログラミング言語の多くではマイナスに2の補数を利用した表現を用いています。
この表現を使うと1000 0001
は**-127**になります。
さて、同じデータから129・‐1・‐127と違う整数が出来てしまいました。
ここから分かる通り、生のデータだけではそれが何を表しているのか分からないのです。
その意味不明な0と1の羅列が、何を表しているのかを定めているのがデータ型というわけです。
var value = 1000 0001;
// 129? -1? -127?
// データ型が分からないので値が定まらない。
var value: int8 = 1000 0001;
// -127
// 「int8(8bit符号付き整数)」というデータ型が分かるので、値が定まる。
ユーザビリティを向上させるためのデータ型
上で説明した例は機械寄りのデータ型でしたが、ユーザビリティを向上させるための人間寄りのデータ型も存在します。
例えば、引数の値によって上下左右に移動するという関数を考えてみましょう。
デベロッパーが想定する引数は"up"
"right"
"down"
"left"
だけですが、JSでは想定外の値も渡せてしまいます。
function move(direction) {
// 左右上下で処理を変える
switch (direction) {
case "up":
case "right":
case "down":
case "left":
}
}
// 想定外の値も引数に渡せてしまう。
move("top");
move(2);
move({up: true});
このような問題を解決するため、TSでは"up"
"right"
"down"
"left"
しか代入できない型を定義して、引数にはその型しか受け付けないようにできます。
// "up"・"right"・"down"・"left"しか代入できない型を定義
type Direction = "up" | "right" | "down" | "left";
function move(direction: Direction) {
switch (direction) {
case "up":
case "right":
case "down":
case "left":
}
}
move("up"); // OK
move("top"); // エラー!
move(2); // エラー!
move({up: true}); // エラー!
このように型を定義することでユーザーの間違いを、機械が教えてくれるようになります。
また、機械側も入力されるべき値を知ることができるので、インテリセンス(自動補完)を使ってユーザーを補助してくれます。
データ型は機械と人とを繋ぐ架け橋の役割を果たしてくれます。
機械と人、両方が知っておくべき情報をデータ型という形で共有できるのです。
TypeScriptのデータ型
それではいよいよ、TSのデータ型の紹介です。
TSにはいろいろな型がありますが、よく利用するものを厳選してお伝えします。
プリミティブ型
TypeScriptにおいて一番基本となる型です。
これはJavaScriptのプリミティブの型と対応しており、string
number
boolean
symbol
bigint
null
undefined
があります。これらは互いに違う型のため、相互に代入することができません。
データ型 | 説明 |
---|---|
string | 文字列 |
number | 数値 |
boolean | 真偽値 |
symbol | ユニークなシンボル |
bigint | 巨大整数 |
null | 該当値無しを表す |
undefined | 未定義を表す |
リテラル型
プリミティブ型の派生としてリテラル型があります。具体的な値しか入れることのできない型です。
これ単体で使用することは少なく、後述のユニオン型と一緒に使いうことが多いです。
let category: "hoge";
category = "hoge"; // OK
category = "fuga"; // エラー!
let five: 5;
five = 5; // OK
five = 0; // エラー!
オブジェクト型
JS・TSのオブジェクト型は、プリミティブ型以外全ての物を指します。
JSのオブジェクトは、自由度が高く便利です。
その反面、自由度が高すぎてコードの使い方が分からないなんてことが、往々にして起こります。
そんな破天荒なJSを、品行方正にするのがTSの仕事です。
TSにはJSに型を付けるための、様々な機能が用意されています。
しかし、とりあえずはclass構文とinterface構文を覚えればOKです。
class構文
JSにもクラスは存在するので詳細は省きます。
JSとTSではプロパティ定義の文法が異なるので気を付けましょう。
また、アクセス修飾子はJSにはない概念で、アクセス修飾子を付けた物がどこからアクセス可能かを指定します。
クラス内だけで使用する場合はprivate
、外部から使用して欲しい場合はpublic
を指定します。
class MyClass {
constructor() {
this.property = "abc";
}
method() {
}
}
class MyClass {
private property: string = "abc";
public method(): void {
}
}
interface構文
インターフェイスを使うと、オブジェクトの型に名前を付けることができます。
例えば引数の型に使うことで、オブジェクトが必要なプロパティを持っていることを保証できます。
interface MyObj {
hoge: string;
fuga: number;
}
function func(arg: MyObj): void {
const a = arg.hoge;
const b = arg.fuga;
}
func({hoge: "abc", fuga: 2}); // OK
func({hoge: "abc", piyo: 2}); // fugaがないのでエラー!
func({hoge: 1, fuga: 2}); // hogeが文字列型じゃないのでエラー!
これだけだと「クラスでもいいんじゃないか?」となりますが、クラスと比べてインターフェイスは責任の範囲が狭めです。
今回の例ですと、オブジェクトが文字列型のhoge
と数値型のfuga
を持っていますよ、という小さい約束なのです。
約束さえ守っていれば、ユーザーは自由にオブジェクトを作ることができます。
// インターフェイスを実装したクラス
class ClassA implements MyObj {
public hoge: string = "";
public fuga: number = 0;
public funcA() {}
}
const objA = new ClassA();
// インターフェイスを実装したクラスの派生クラス
class ClassB extends ClassA {
public funcB() {}
}
const objB = new ClassB();
// 分かりにくいので推奨できないが、明示的にインターフェイスを実装しなくても行ける
class ClassC {
public hoge: string = "";
public fuga: number = 0;
}
const objC = new ClassC();
// もちろん、オブジェクトリテラルでもOK
const objD = { hoge: "abc", fuga: 2 };
// 約束を守っているので、全てOK
func(objA);
func(objB);
func(objC);
func(objD);
配列型
TSにおいては配列はオブジェクトの一種です。ただし、配列の型を表すための特別な文法が用意されています。配列の型を表すためには[]
を用います。
let hoge: number[];
関数型
JSではコールバック関数をよく利用しますが、そういったものに対応するために関数にも型が存在します。
例えば、引数hoge: string
とfuga: number
を持つ、戻り値boolean
型の関数は、(hoge: string, fuga: number) => boolean
と表現します。
書き方がラムダ式と似ていますが、別物ですので注意しましょう。
function onLoaded(callback: (hoge: string, fuga: number) => boolean) {
// 省略
}
onLoaded((hoge, fuga) => {
return true;
});
ジェネリクス型
ジェネリクス型は、型だけ違って処理の内容が同じようなものを作るときに使います。
一例としてはこんな感じになります。
interface Generics<T> {
hoge: T;
}
const objA: Generics<string> = {
hoge: "abc"
};
const objB: Generics<number> = {
hoge: 123
};
使い所が難しいため、ジェネリクス型を定義して使うことはあまりないと思います。
ただ、組み込みのクラスやライブラリなどで利用されていますので、使い方だけは覚えておきましょう。
以下はArray<T>
クラスの例になります。
const names: Array<string> = new Array<string>("太郎", "次郎", "三郎");
ユニオン型
ユニオン型は複数の型のどれかに当てはまるような型を表しています。
例えば、string | number
という型は「stringまたはnumberである値の型」になります。
let value: string | number;
value = 100; // OK
value = "hoge"; // OK
value = true; // エラー!
JSでは要素が見つからなかった場合にnullを返すというnullableな値を扱うことが良くあります。
TS的にはnullとその他の型は別物ですので、そういった際はユニオン型を使って表現します。
// 要素が見つかればElementオブジェクトが返り、見つからなければnullが返る。
const item: Element | null = document.querySelector("");
更にリテラル型と組み合わせることで、特定の値しか受け付けない型を作る事もできます。
let value: "hoge" | "fuga";
value = "hoge"; // OK
value = "fuga"; // OK
value = "piyo"; // エラー!
ユニオン型の注意点
nullableな値を扱う際にユニオン型を使うという説明をしました。
例えばDOMのquerySelectorメソッドは、要素が見つかればElementオブジェクトを返し、見つからなければnullを返しますが、戻り値のデータ型はあくまでElement | null
型です。Element
型でもnull
型でもありません。
ですから、例えばElementオブジェクトのtagNameプロパティにアクセスしたくても、もしnullだった場合にtagNameプロパティが存在しないためエラーになります。
const item: Element | null = document.querySelector("");
console.log(item.tagName); // エラー! nullの場合もあるのでtagNameプロパティは使えない。
ではどうするのかというと、オブジェクトがnullだった場合にreturn
やthrow
をしてやり、オブジェクトがnullでないことを証明すればElement
型として利用することができます。
const item: Element | null = document.querySelector("");
// nullチェック
if(item === null) {
throw new Error();
}
console.log(item.tagName); // nullの場合、ここには到達できないのでOK!
いちいちnullチェックするは辛いという場合は、?.
(オプショナルチェイニング)が使えます。
オプショナルチェイニングは、nullableなオブジェクトに対してnullチェックなしにプロパティにアクセス事が出来ます。
もし、?.
の前の部分がundefinedあるいはnullであれば検査をストップして、undefinedを返します。
オプショナルチェイニングを利用する場合でも、undefinedが返ってくるかもしれないと考えてコードを書きましょう。
console.log(item?.tagName); // OK. nullの場合はエラーにならずにundefinedが返る。
最小構成でTypeScriptを使ってみる
ここからは、実際にTSを触ってみましょう。まずは最小構成で行きます。
任意のフォルダを作り、ターミナルでそのフォルダに移動してください。
TSのトランスパイルはNode環境で行うので、npmコマンドでプロジェクトの初期化とTSパッケージのインストールを行います。
$ npm init -y
$ npm i -D typescript
これで環境準備ができたので、さっそくTSのコードを書いていきます。
以下は適当なコードですが、オブジェクトのメソッドにアクセスする際にインテリセンスが効いたり、間違った引数を渡すと怒られたりするのを確認してみてください。
type MyRole = "user" | "admin" | "developer";
class MyClass {
getGreeting(role: MyRole): string {
return `Hello, ${role}!`;
}
}
const myObj = new MyClass();
const greeting = myObj.getGreeting("user");
console.log(greeting);
コードを書き終わったら、トランスパイルを行います。
トランスパイルするにはtypescriptパッケージのtsc
コマンドを利用します。以下のコードを実行してください。
index.js
が生成されます。
npx tsc ./index.ts
後はこのファイルをブラウザで動かすなり、Nodeで動かすなりすればOKです。
今回はNodeで実行してみましょう。
node ./index.js
TypeScriptの設定
とりあえず最小構成でTSを利用してみましたが、実際の開発ではTSの設定が必要になります。
TSの設定はプロジェクトルートにtsconfig.json
を設置することで行えます。
ただ、TypeScriptは設定が面倒です。超面倒。
いや、TypeScriptの設定だけならまだいいんですが、BabelやらWebpackとの兼ね合いがどうのとか、出力するモジュール形式がどうとか、VueやらReactやらとなってくると手に負えません。
幸いなことに、Next.js
やElectron
といったフレームワークではTypeScriptのテンプレートを用意してくれています。実際の開発ではそういったテンプレートを使うことを強くお勧めします。
気軽にTSを書きたい時の設定
とはいえ、TSを書きたい時にいちいちフレームワークを使うのは重たすぎるので、「最低限これは必要かな~」という設定を紹介しますので参考にしてみてください。
以下のファイルをプロジェクトルートに設置してnpx tsc
コマンドを実行すると、dist
フォルダにトランスパイルされたファイルが出力されます。
また、npx tsc --init
コマンドを使うとtsconfig.json
が自動生成されるので、それも参考になります。
{
"compilerOptions": {
"target": "es2020", /* 出力されるjsのバージョン。ブラウザとNode14はes2020をサポートしているので"es2020"を指定。 */
"module": "es2020", /* どのモジュール方式で出力されるか。 ブラウザ標準に合わせて"es2020"を指定。 */
"declaration": true, /* 型定義ファイル(.d.ts)を生成する。 */
"sourceMap": true, /* ソースマップファイルを生成する。 */
"outDir": "./dist", /* トランスパイルされたファイルの出力先。 */
"strict": true, /* 厳格な型チェックを有効にする。 */
},
"exclude": [ /* プロジェクトに含めないフォルダを指定する */
"node_modules",
"./dist"
]
}
おわりに
TSを使うのに必要そうな情報をザーッと書いてみましたが、いかがだったでしょうか?
ここまで書いておいて何ですが、JSが使えるなら「習うより慣れろ」の精神で行った方が、習得が速いかもしれません。
環境構築の煩雑さがTSの入門を妨げている気がするので、設定などは適当で良いので兎にも角にもTSを使ってみましょう!!