TypeScript
JavaScriptとデータ型
次のような関数を考えてみましょう。
function formatDate(date) {
return `${date.getFullYear()}/${date.getMonth() + 1}/${date.getDate()}`;
}
この関数のdate
引数には、どのような値を指定すれば良いでしょうか。答えは、Date
オブジェクトを指定することです。formatDate(new Date("2022-01-01"))
は動作しますが、formatDate("2022-01-01")
はエラーになってしまいます。しかも、エラーが発生するかどうかは実際に実行してみるまでわかりません。
上のような単純なプログラムならこういった問題は起きにくいですが、プログラムの規模が大きくなるにつれ、「どういった値がやりとりされているのか」という情報を把握することが重要になってきます。こういった情報を、データ型、あるいは単に型と呼びます。
TypeScriptを用いると、プログラム中にデータ型を記述できるようになります。TypeScriptは、Microsoft社によって開発された、JavaScriptにトランスパイルして用いられる言語です。
TypeScriptにおける型は、通常:
の記号に続けて記述します。例えば、先程のプログラムをTypeScriptを用いて書き直すと、次のようになります。引数の部分に型指定が入っているところに注目してください。
function formatDate(date: Date) {
return `${date.getFullYear()}/${date.getMonth() + 1}/${date.getDate()}`;
}
これにより、次のような開発者体験が得られます。
date.
と入力されたタイミングで、使用可能なメソッドが全て表示されます- 誤った型の引数 (動画内では文字列) を指定すると、エラーが表示されるようになります
C++やJavaなどの一般的なプログラミング言語では、型の情報は実行に何らかの影響を与えますが、TypeScriptはJavaScriptにトランスパイルされる言語であり、実行時には型の情報は一切利用されません。
TypeScriptを使ってNode.jsのプログラムを記述する
TypeScriptを用いてNode.jsのプログラムを作成するには、次の手順に従ってください。
まずは、プロジェクトルートにpackage.json
を作成します。npm init
を実行すればよいのでした。
続いて、
npm install -D typescript
を実行し、typescriptパッケージをインストールします。-D
オプションは「開発時のみに使用する」という意思表示になります。package.json
に記録される方法が少しだけ変わります。
続いて、main.ts
ファイルを作成します。TypeScriptファイルの拡張子は通常.ts
です。今回は、
const language: string = "TypeScript";
console.log(`Hello ${language}!`);
としました。
TypeScriptファイルの作成が終わったら、npx
コマンドでTypeScriptパッケージを実行し、TypeScriptファイルをJavaScriptファイルにトランスパイルします。パッケージ名と異なり、tsc
となるので注意しましょう。
npx tsc main.ts
すると、同名のJavaScriptファイルが生成されます。このファイルを実行すれば、通常のJavaScriptとして実行できます。
TypeScriptの基礎
TypeScriptを試すには、Microsoftが提供しているTS Playgroundを用いると便利です。必要に応じて利用してください。
型を記述できる場所
TypeScriptの型は、関数の引数や戻り値、変数の後に:
とともに記述できます。
// addはnumber型の引数a, bをとりnumber型の値を返す関数
function add(a: number, b: number): number {
return a + b;
}
// sumはnumber型の変数
let sum: number = add(3, 4);
データ型が誤っている場合、TypeScriptはエラーを出力します。
sum = "7"; // Type 'string' is not assignable to type 'number'.
add("3", "4"); // Argument of type 'string' is not assignable to parameter of type 'number'.
データ型と値
TypeScriptのデータ型は、全ての値を含む集合unknown
の部分集合になります。ある値v
が集合T
に属するとき、v
はT
型であるといいます。例えば、数値1
は1
型、number
型、unknown
型のいずれにも当てはまります。なお、空集合はnever
型です。
// すべて正しい
const a: unknown = 1;
const b: number = 1;
const c: 1 = 1; // 左辺の1はデータ型 (unknownの部分集合) としての1
// never型にはどんな値も代入できない
// const d: never = 1;
any
型TypeScriptの標準設定では、型が判明しなかった場合、any
型が指定されたものとみなされます。any
型の値には、どんな操作でも許容されます。any
型の値はどんな型の変数にも代入できますし、any
型の変数にはどんな値でも代入できます。上の集合のどの部分にも当てはまりません。
const strangeValue: any = 1;
// TypeScriptは誤りを検出できないが、実行時にエラーになる
strangeValue.strangeMethod();
データ型の別名
type
宣言を用いると、データ型に対して別名を付けられます。
type Age = number;
// 変数ageはAge (number) 型
const age: Age = 18;
型の名前には通常パスカルケースが用いられます。
オブジェクト型
オブジェクト型では、プロパティの名前や、値の型が指定できます。
// Studentはstring型のnameプロパティとnumber型のageプロパティを持つオブジェクト
type Student = {
name: string;
age: number;
};
let student: Student = { name: "田中", age: 18 };
なお、余分なプロパティを持つオブジェクトでも問題なく代入できます。次の例から、Teacher
はStudent
の部分集合であることが分かります。
type Teacher = {
name: string;
age: number;
subject: string;
};
let teacher: Teacher = { name: "鈴木", age: 18, subject: "数学" };
student = teacher;
// Property 'subject' is missing in type 'Student' but required in type 'Teacher'.
teacher = student;
配列型
型T
の配列型は、T[]
のように記述できます。また、T
がU
の部分集合であれば、T[]
はU[]
の部分集合になります。
const numbers: number[] = [1, 2, 3];
// number[]はunknown[]の部分集合
const unknowns: unknown[] = numbers;
関数型
関数型では、引数や戻り値の型が指定できます。引数名は異なっていても同じ型だとみなされます。
// BinaryNumberOperatorはnumber型の引数2つを受け取ってnumber型の値を返す関数
type BinaryNumberOperator = (x: number, y: number) => number;
function add(a: number, b: number): number {
return a + b;
}
const operator: BinaryNumberOperator = add;
引数の数が少ない関数型は、多い関数型の部分集合とみなされます。
function increment(a: number): number {
return a + 1;
}
// (a: number) => numberは(a: number, b: number) => numberの部分集合
const operator2: BinaryNumberOperator = increment;
型演算
2 つの型に対し、集合の和や積 (共通部分)を求める記号が利用できます。
記号 | 意味 |
---|---|
& | 共通部分 |
| | 合併 |
type Student = { name: string; major: string };
type Programmer = { name: string; language: string };
const studentProgrammer: Student & Programmer = {
name: "田中",
major: "数学",
language: "TypeScript",
};
const hand: "グー" | "チョキ" | "パー" = "グー";
型推論
文脈からデータ型が明らかな場合は、型定義の記述を省略できます。
// ageはnumber型
let age = 18;
// Type 'string' is not assignable to type 'number'.
age = "19";
// 戻り値の型が推論されるため、addは(a: number, b: number) => number型
function add(a: number, b: number) {
return a + b;
}
関数型を要求する部分に関数式を指定する場合、その引数の型が推論されます。
type BinaryNumberOperator = (a: number, b: number) => number;
// aやbはnumberに推論される
const operator: BinaryNumberOperator = (a, b) => a + b;
// イベントハンドラの記述の際に便利
window.onload = (e) => {
// eはEvent型
};
ジェネリクス
引数を一つ受け取り、その値をそのまま返す関数を考えてみよう。
function identity(x) {
return x;
}
こういった関数では、引数x
はどんな型の値も指定できます。つまり、x
はunknown
型とするのが適切なはずです。しかしながら、引数をunknown
型としてしまうと、戻り値がunknown
型となってしまい、戻り値に対する操作が一切不可能になってしまいます。
function identity(x: unknown) {
return x;
}
// Object is of type 'unknown'.
identity(1).toString();
TypeScriptでは、型パラメータを用いることで、この問題を解決できます。型パラメータは、通常の引数と異なり、型を指定するための特殊な引数です。JavaScriptにトランスパイルされるタイミングで削除されます。
// Tは型パラメータ
// identityはT型の引数を受け取ってT型の戻り値を返す関数
function identity<T>(x: T): T {
return x;
}
// Tにnumberを指定したので、ここではidentityはnumber型の引数を受け取ってnumber型の戻り値を返す関数
identity<number>(1).toString();
// 文脈から型パラメータが明らかな場合は推論される
// この場合はTはnumberに推論される
identity(1).toString();
こういった言語機能は他の多くのプログラミング言語でも用意されており、ジェネリクスと呼ばれます。
type
宣言でも型パラメータを利用できます。
type BinaryOperator<T> = (a: T, b: T) => T;
// addは(a: number, b: number) => number型
const add: BinaryOperator<number> = (a, b) => a + b;
TypeScriptとnpm
npmでインストールしたパッケージがTypeScriptに対応している場合、下の図のように、npmのパッケージのウェブサイトに アイコンが表示されます。
DT
アイコンがついているパッケージは、@types/パッケージ名
という名称のパッケージをインストールすることで、TypeScriptからパッケージが利用可能になります。例えば、@types/express
パッケージをインストールすることにより、express
パッケージがTypeScriptから利用できるようになります。
@types
パッケージのインストール前後でapp
の型が変わっていることが分かります。
フロントエンドにおけるTypeScriptの利用
Viteは、標準でTypeScriptのトランスパイラが内蔵されています。新しくプロジェクトを作成する際は、テンプレートを選択する際にTypeScriptのテンプレートを使用しましょう。
tsconfig.json
この方法でプロジェクトを作成すると、tsconfig.json
というファイルが生成されます。TypeScriptは、さまざまなJavaScriptのニーズに合わせてカスタマイズできるようになっており、その設定を記述するためのファイルがtsconfig.json
です。
公式ドキュメントには、全てのオプションの詳細な説明が記述されています。特に、strict
オプションは、TypeScriptの能力を大幅に上昇させることができるので、有効にすることが推奨されています。typescript
パッケージを直接インストールしたプロジェクトでは、npx tsc --init
コマンドによりこのファイルを生成できます。
演習問題1
-
string & number
型は何型と等しいでしょうか。 -
次のように定義される型
T
に対して使用可能なプロパティは何でしょうか。type T = { name: string; age: number } | { name: string; subject: string };
-
次の型のうち、
(v: string) => string
型とみなせる (部分集合である) ものを全て選んでください。(v: unknown) => string
(v: never) => string
(v: string) => unknown
(v: string) => never
-
次の関数
apply
は、関数を適用する関数です。ジェネリクスを用いて適切な型をつけてください (ヒント: 引数と戻り値を表す型パラメータを定義しましょう)。function apply(f, x) {
return f(x);
}
解答例
-
never
型type StringAndNumber = string & number; // never
-
name
のみ2つの型に共通しているのは
name
プロパティだけなので、T
型の変数に必ず存在しているプロパティはname
のみとなります。よって、name
のみ使用可能となります。 -
(v: unknown) => string
と(v: string) => never
まず
(v: string) => never
に関してですが、こちらはなんとなく想像がつくかもしれません。never
型はすべての型に含まれるためstring
型にも含まれますから、(v: string) => string
とみなすことができるでしょう。一方で、
(v: unknown) => string
型が答えになっているのは意外かもしれません。unknown
型はstring
型を含むから間違いなのではないかと考えた方も多いでしょう。しかし、この理論で行くと少々不都合が生じます。例えば、次のようなコードを考えましょう。type F = (arg: { name: string, math: number }) => number;
function func(arg: { name: string, math: number, science: number }): number {
console.log(arg.science);
return arg.math;
}
const f: F = func;
f({ name: "Tanaka", math: 100 });このコードでは、
{ name: string, math: number }
型は{ name: string, math: number, science: number }
型を含んでいます。先ほどのunknown
型とstring
型の関係と同じです。もしこのコードが通る場合、実際に渡された
{name: "Tanaka", math: 100}
には存在しないはずのscience
プロパティにアクセスできてしまうことになります。このようなことを防ぐために、引数の型が小さい集合になればなるほど、関数の型は大きな集合になる必要があります。 -
以下のコード
function apply<T, U>(f: (x: T) => U, x: T): U {
return f(x);
}
演習問題2(発展)
フロントエンド・バックエンドともにTypeScriptを利用するアプリケーションを作成し、公開してみてください。