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

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()}`;
}

TypeScriptを導入することにより、このプログラムを記述する際に、次のような支援が得られます。

  • 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 のウェブサイトで提供されているTS Playgroundを用いると、ブラウザ上で TypeScript のコードを記述し、型チェックの結果を確認できます。

型を記述できる場所

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におけるデータ型は、その型の値として扱うことのできる全ての値の集合です。TypeScriptは、プログラムの全体を検査し、指定された型から外れた値の入る可能性のある箇所を検出して、エラーを発生させます。

TypeScriptの型には包含関係があり、TUの部分集合である場合、つまり、ある型Tの全ての値が別の型Uの値でもある場合、TU部分型であるといいます。TypeScriptでは、全体集合であるunknown型から、空集合であるnever型まで、さまざまな部分型が定義されています。

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

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

TypeScriptのデータ型

any

TypeScriptの標準設定では、型が判明しなかった場合、any型が指定されたものとみなされます。any型の値には、どんな操作でも許容されます。any型の値はどんな型の変数にも代入できますし、any型の変数にはどんな値でも代入できます。上の集合のどの部分にも当てはまりません。

const strangeValue: any = 1;

// TypeScriptは誤りを検出できないが、実行時にエラーになる
strangeValue.strangeMethod();

確認問題

JavaScriptで記載された次の関数repeatに対し、TypeScriptとして適切と思われる型を追記してください。

function repeat(text, times) {
let result = "";
for (let i = 0; i < times; i++) {
result += text;
}
return result;
}
解答例: 文字列の繰り返し

関数 repeat は、文字列 texttimes 回繰り返して結合した新しい文字列を返す関数です。したがって、textstring 型、timesnumber 型、戻り値も string 型とするのが適切です。

function repeat(text: string, times: number): string {
let result: string = "";
for (let i: number = 0; i < times; i++) {
result += text;
}
return result;
}

データ型の別名

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

クラス名は、そのまま型名として利用できます。また、フィールドにも型を指定できます。

class Student {
name: string;
age: number;

constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
}

const student: Student = new Student("田中", 18);

TypeScriptでは、プロパティが多いオブジェクト型は、プロパティが少ないオブジェクト型の部分型とみなされます。次の例では、プロパティの数が多いTeacherオブジェクトを、プロパティの数が少ないStudentとして代入しています。

これは、TeacherオブジェクトがStudentオブジェクトの全てのプロパティを持っており、Studentオブジェクトに対する全ての操作を安全に実行できるためです。一方で、StudentオブジェクトをTeacherオブジェクトとして代入することはできません。なぜなら、StudentオブジェクトはTeacherオブジェクトが持つsubjectプロパティを持っていないためです。

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;

確認問題

JavaScriptで記載された次のプログラムに対し、TypeScriptとして適切と思われる型を追記してください。

class Product {
name;
price;
constructor(name, price) {
this.name = name;
this.price = price;
}
}

class Book extends Product {
author;
constructor(name, price, author) {
super(name, price);
this.author = author;
}
}

function calculateTotal(item, quantity) {
return item.price * quantity;
}

const book = new Book("TypeScript 入門", 2500, "山田 太郎");
const total = calculateTotal(book, 3);
解答例: 商品の合計金額の計算
  • name, price, author はそれぞれ string, number, string 型とするのが適切です。
  • calculateTotal 関数の引数 itemProduct 型と Book 型のいずれも文法的には正しいですが、関数の処理において Book 特有のプロパティを使用していないため、Product 型を指定するのがより適切です。
  • quantity は商品の数量を表すため number 型とします。戻り値も合計金額を表すため number 型とします。
class Product {
name: string;
price: number;
constructor(name: string, price: number) {
this.name = name;
this.price = price;
ï;
}
}

class Book extends Product {
author: string;
constructor(name: string, price: number, author: string) {
super(name, price);
this.author = author;
}
}

function calculateTotal(item: Product, quantity: number): number {
return item.price * quantity;
}

const book: Book = new Book("TypeScript 入門", 2500, "山田 太郎");
const total: number = calculateTotal(book, 3);

配列型

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;

関数型の部分型の関係は複雑ですが、次の表のように整理できます。

種類関係関数型の関係
引数の数f1 < f2f1f2 の部分型
引数の型TU の部分型(x: U) => R(x: T) => R の部分型
戻り値の型TU の部分型() => T() => U の部分型

戻り値の型については自然に理解できますが、引数の数や引数の型については直感に反するかもしれません。このような関係になっているのは、関数が受け取る引数が少なかったり、より一般的な型であったりする場合、その関数はより多くの状況で安全に使用できるためです。

// 引数の数が少ない関数は、引数の数が多い関数の部分型
type BinaryNumberOperator = (x: number, y: number) => number;
const increment: BinaryNumberOperator = (x) => x + 1;
increment(1, 2); // 第2引数は無視されるので問題ない

// 引数の型がより一般的な関数は、引数の型がより具体的な関数の部分型
type NumberConsumer = (number: number) => unknown;
const logger: NumberConsumer = (number: unknown) => console.log(number);
logger(1); // number型の引数はunknown型として扱えるので問題ない

確認問題

次の型のうち、(v: string) => string型の部分型であるものを全て選んでください。

  • (v1: string, v2: string) => string
  • () => string
  • (v: unknown) => string
  • (v: never) => string
  • (v: string) => unknown
  • (v: string) => never
解答例

引数の数は少ないものが、引数の型はより一般的なものが、戻り値はより具体的なものが、部分型となります。

  • () => string
  • (v: unknown) => string
  • (v: never) => string

型演算

2 つの型に対し、集合の和や積 (共通部分)を求める記号が利用できます。

記号意味
&共通部分
|合併

次の例では、Student型とProgrammer型の共通部分を持つオブジェクトstudentProgrammerを定義しています。TypeScriptでは、多くのプロパティを持つオブジェクト型は、少ないプロパティを持つオブジェクト型の部分型とみなされるため、Student & Programmer型は、namemajorlanguageの3つのプロパティを持つオブジェクト型と見做されます。

type Student = { name: string; major: string };
type Programmer = { name: string; language: string };
const studentProgrammer: Student & Programmer = {
name: "田中",
major: "数学",
language: "TypeScript",
};

合併型は、いずれかの型に属する値を表します。次の例では、Meeting型はInPersonMeeting型かOnlineMeeting型のいずれかに属する値を表します。関数describeMeetingでは、typeプロパティの値に応じて適切な処理を行っています。

TypeScriptでは、変数が文脈から合併型のどの型に属するかが明らかな場合、その型に応じたプロパティやメソッドが利用可能になります。この例では、typeプロパティの値が"in_person"である場合、locationプロパティが利用可能であり、"online"である場合、urlプロパティが利用可能であることが分かります。

type InPersonMeeting = { type: "in_person"; location: string };
type OnlineMeeting = { type: "online"; url: string };
type Meeting = InPersonMeeting | OnlineMeeting;

function describeMeeting(meeting: Meeting): string {
if (meeting.type === "in_person") {
return `会議は ${meeting.location} で行われます。`;
} else {
return `会議はオンラインで行われます。URL: ${meeting.url}`;
}
}

確認問題

問題1: string & number型は何型と等しいでしょうか。

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

type T = { name: string; age: number } | { name: string; subject: string };
解答例

問題1: never

TypeScriptでは、string型とnumber型とも見做せるような値は存在しません。したがって、string & number型は空集合であるnever型と等しいです。

問題2: nameのみ

Tは、{ name: string; age: number }型と{ name: string; subject: string }型の合併型です。両方の型に共通して存在するプロパティはnameのみであるため、型Tに対して使用可能なプロパティはnameのみとなります。

型推論

文脈からデータ型が明らかな場合は、型定義の記述を省略できます。

// 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宣言でも型パラメータを利用できます。

class Range<T> {
from: T;
to: T;
constructor(from: T, to: T) {
this.from = from;
this.to = to;
}
}

const dateRange = new Range<Date>(
new Date("2022-01-01"),
new Date("2022-12-31"),
);

type BinaryOperator<T> = (a: T, b: T) => T;

// addは(a: number, b: number) => number型
const add: BinaryOperator<number> = (a, b) => a + b;

確認問題

次の関数applyは、関数を適用する関数です。引数と戻り値を表す型パラメータを定義し、ジェネリクスを用いて適切な型をつけてください。

function apply(f, x) {
return f(x);
}

const result = apply(Math.sqrt, 1024); // resultはnumber型
解答例

Math.sqrtは、(x: number) => number型の関数です。したがって、apply関数の型パラメータTUnumberに推論されます。

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

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コマンドによりこのファイルを生成できます。