メインコンテンツまでスキップ

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 です。今回は、

main.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 に属するとき、vT 型であるといいます。例えば、数値 11 型、number 型、unknown 型のいずれにも当てはまります。なお、空集合は never 型です。

// すべて正しい
const a: unknown = 1;
const b: number = 1;
const c: 1 = 1; // 左辺の 1 はデータ型 (unknown の部分集合) としての 1

// never 型にはどんな値も代入できない
// const d: never = 1;

TypeScript のデータ型

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 };

なお、余分なプロパティを持つオブジェクトでも問題なく代入できます。次の例から、TeacherStudent の部分集合であることが分かります。

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[] のように記述できます。また、TU の部分集合であれば、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 はどんな型の値も指定できます。つまり、xunknown 型とするのが適切なはずです。しかしながら、引数を 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 のパッケージのウェブサイトに アイコンが表示されます。

npm パッケージの TypeScript 対応

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 型は何型と等しいでしょうか。

  2. 次のように定義される型 T に対して使用可能なプロパティは何でしょうか。

    type T = { name: string; age: number } | { name: string; subject: string };
  3. 次の型のうち、(v: string) => string 型とみなせる (部分集合である) ものを全て選んでください。

    • (v: unknown) => string
    • (v: never) => string
    • (v: string) => unknown
    • (v: string) => never
  4. 次の関数 apply は、関数を適用する関数です。ジェネリクスを用いて適切な型をつけてください (ヒント: 引数と戻り値を表す型パラメータを定義しましょう)。

    function apply(f, x) {
    return f(x);
    }
  5. フロントエンド・バックエンドともに TypeScript を利用するアプリケーションを作成し、公開してみてください。

解答例
  1. never

    type StringAndNumber = string & number; // never
  2. name のみ

    2 つの型に共通しているのは name プロパティだけなので、T 型の変数に必ず存在しているプロパティはname のみとなります。よって、name のみ使用可能となります。

  3. (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 プロパティにアクセスできてしまうことになります。このようなことを防ぐために、引数の型が小さい集合になればなるほど、関数の型は大きな集合になる必要があります。

  4. 以下のコード

    function apply<T, U>(f: (x: T) => U, x: T): U {
    return f(x);
    }