TypeScriptを学習する前の人の中にはTypeScriptがJavaScriptの開発には必須な知識だということはわかってはいるものの以下のような疑問を持っている人は多いのではないでしょうか。

  • TypeScriptがなぜ必要なのか
  • TypeScriptを習得するためにはJavaScriptとは異なる言語を一から勉強をしなければならないのか
  • TypeScriptは難しいか

そのような疑問を持っている人がTypeScriptを実践で活用できることを目標に本文書を作成しています。

TypeScriptとは

TypeScriptはJavaScriptにType(型)を扱う機能が追加されたプログラミング言語です。JavaScriptとは異なる言語ではなくType(型)に関すること以外のコードについては通常のJavaScriptと同様の構文を使って記述することができます。TypeScriptはJavaScriptの機能をすべて利用できることからスーパーセットと呼ばれます。TypeScriptを学習することは何か新しいことを一から学ぶのではなくJavaScriptの知識にTypeScriptのType(型)の設定の知識を加えていくことになります。

JavaScriptにType(型)が扱えるようになることがなぜそんなに大事かと疑問に思う人も多いかと思います。JavaScriptではType(型)を事前に宣言することがないので誤ったType(型)のデータを関数に渡している場合(文字列を渡さないければならないのに数値を渡す)実行中になってようやくエラーであることがわかります。TypeScriptはType(型)を宣言するだけではなく事前にコンパイルにより型チェックを行うので、Type(型)に問題がある場合はコードを動作させる前に問題を把握することができます。またエディターによってはType(型)に関する問題がある場合はコードの記述中にエラーメッセージで問題がある箇所を示してくれるのでコード記述中に問題に気付いて修正を行うことができます。コードを記述する際のメリットだけではなく例えば関数であれば引数と戻り値にType(型)を設定することができるので他のプログラマーがコードを見た時にどのような値を与えてどのような値が戻ってくるのかを理解することができドキュメントとしての役割を持つこともできます。

TypeScriptを利用する目的はエラーの少ない品質の高いコードを効率よく記述することにあります。JavaScriptを勉強を始めたばかりでまずはコードが動くことを目的にする場合はそれほど重要ではありませんし個人で開発するのであれば使わないという選択もあります。しかし、対価をもらって行う仕事であれば納品するコードの品質が高ければ高いほどいいのは明白なのでTypeScriptを利用するということが選択肢に入ってきます。大規模なプロジェクトにプログラマーとして参加するためにはTypeScriptの理解は必須になります。

TypeScriptのインストール

TypeScriptを利用するためにはTypeScriptが動作する環境を構築する必要があります。手元で利用しているWindowsやMacで利用する場合はNode.jsのインストールが必須となります。

Node.jsとnpmがインストールされている環境でnpmコマンドでグローバルにTypeScriptのインストールを行います。


 $ npm install -g typescript

インストールが完了したらtsc -vを実行するとインストールしたTypeScriptのバージョンを確認することができます。tscはTypeScript Compiler(コンパイラ)の略です。


 % tsc -v
Version 4.5.4

指定したバージョンをインストールしたい場合はtypescript@4.5.3のように@の後ろにバージョンを指定してください。

TypeScriptのバージョンアップが必要な場合や最新版にアップしたい場合はtypescriptに@latestを追加することで最新版にアップデートを行うことができます。


 % npm install -g typescript@latest
 % tsc -v
 Version 4.5.5
エディターについては好みの分かれるところではありますがTypeScriptはMicrosoftが開発しているためVisual Studio Codeがおすすめです。

最も簡単な例を使った動作確認

動作確認のためのサンプルコードを作成します。任意の名前のフォルダの作成してそのフォルダの下にgreeter.tsファイルを作成し下記のコードを記述します。TypeScriptのファイルの拡張子はtsです。


function greeter(person) {
  return "Hello, " + person;
}

let user = "Jane Doe";

document.body.textContent = greeter(user);

はじめてのコンパイル

TypeScriptで記述したコードはコンパイルを行いJavaScriptファイルへの変換が必要となります。作成したgreeter.tsファイルをtscコマンドを利用してコンパイルします。


 $ tsc greeter.ts 

実行が完了するとtscコマンドを実行したフォルダ内にコンパイルが完了したgreeter.jsファイルが作成されます。作成されたgreeter.jsファイルを確認するとgreeter.tsファイル内でletで定義したuserがvarに変更になっていることが確認できます。これはコンパイルによってES5(ECMAScript)に変換されるためです。


function greeter(person) {
    return "Hello, " + person;
}
var user = "Jane Doe";
document.body.textContent = greeter(user);
ECMAScriptはJavaScriptの仕様を定めたものでバージョンによってJavaScriptで使える機能や関数が異なります。ES6ではlet, constを利用することができますがES5ではlet, constが利用できないためvarに変更されます。新しい数字ほどバージョンが高く新しい機能が追加されています。ブラウザによってサポートするESのバージョンが異なるので注意が必要です。

ES6など他のバージョンに変換することも可能で設定はTypeScriptの設定ファイルであるtsconfig.jsonファイルで行います。後ほどtsconfig.jsonファイルを作成して動作確認します。

greet.jsファイルをindex.htmlファイルのscriptタグで読み込むとブラウザ画面に”Hello Jane Doe”が表示されます。コンパイル前のgreeter.tsファイルをscriptタグで指定しても同じ結果になります。


<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <script src="./greeter.js"></script>
  </body>
</html>

watchオプション

コードを更新したらtscコマンドを再実行する必要がありますがwatchオプションを利用するとファイルの更新を監視してくれるのでコードを更新する度に手動でtscコマンドを実行する必要がなくなります。


 $ tsc -watch greeter.ts 
[13:56:02] Starting compilation in watch mode...

[13:56:03] Found 0 errors. Watching for file changes.
–watchは-wの省略系を利用できます。

Type Annotation(型アノテーション)

先ほど作成したgreeter.tsファイルは拡張子にtsをつけていましたがTypeScriptの設定は何も行っていませんでした。ここではgreeter関数の引数のpersonに型を明示的に設定するためTypeScriptの型アノテーションを追加します。引数のpersonの右側に:string(:コロン)と追加することでgreeterには文字列が入ることを明示的に示しています。型アノテーションの:stringを設定した場合、引数に文字列を渡さない場合はエラーとなります。


function greeter(person: string) {
  return "Hello, " + person;
}

stringを追加してもこれまでのコードであればuserは文字列の”Jane Doe”なのでエラーが表示されることもなくtscコマンドは型アノテーションを追加する前と同様にコンパイルも完了します。

しかし、意図的にuserに配列を渡してtscコマンドでコンパイルを実行するとエラーが発生します。


function greeter(person: string) {
  return "Hello, " + person;
}

let user = [0, 1, 2];

document.body.textContent = greeter(user);

Visual Studio Codeなどのエディターを利用している場合はコンパイルをする前にエラーのメッセージが表示されるのでどこに問題があるかすぐに気づきます。

tscコマンドを実行すると以下のように文字列が設定されている引数に数値で構成された配列を入れることができないというエラーメッセージが表示されます。どこに問題があるかも指摘してくれるのでどこが間違っているかもわかります。


 $ tsc greeter.ts 
greeter.ts:7:37 - error TS2345: Argument of type 'number[]' is not assignable to parameter of type 'string'.

7 document.body.textContent = greeter(user);
                                      ~~~~
Found 1 error.

エラーが出力されてもJavaScriptへの変換は完了しgreeter.jsファイルは作成されます。変換されたJavaScriptのファイルでは設定した型アノテーションが表示されことはありません。JavaScriptでは型を扱えないためです。

作成されるgreeter.jsファイルをindex.htmlファイルのscriptタグで読み込むと画面には、Hello, 0,1,2と表示され実行できないわけではありません。

型アノテーションの入ったgreeter.tsファイルをindex.htmlのscriptタグで読み込むとUncaught SyntaxError: Unexpected token ‘:’のエラーが発生します。JavaScriptでは型が扱えないため型アノテーションの指定(:コロン)を含むJavaScriptはエラーになります。

ここまでの動作で”TypeScriptはJavaScriptに変換しないとブラウザで利用することができないこと“と“型アノテーションを設定することで誤った型のデータ挿入を未然に防ぐことができること”が確認できました。

tsconfig.jsonファイルの作成

TypeScriptの設定ファイルtsconfig.jsonはtsc –initコマンドで作成することができます。実行と同時にtsconfig.jsonファイルで設定することができるいくつかのオプションのデフォルト値が表示されます。


 % tsc --init

Created a new tsconfig.json with:                                
                                                              TS 
  target: es2016
  module: commonjs
  strict: true
  esModuleInterop: true
  skipLibCheck: true
  forceConsistentCasingInFileNames: true

tsc –initコマンドを実行後に実行したフォルダ内にtsconfig.jsonファイルが作成されます。ファイルを開くと各種設定が記述されています。tsconfig.jsonファイルを作成後はファイル名を指定しなくてもtscコマンドを実行するたけでコンパイルが行われます。


 % tsc 

最初にコンパイルをした時にletで定義した変数がコンパイル後はvarになっていました。tsconfig.jsonファイルにあるtargetオプションが”es2016″になっているのでtscコマンドを実行するとvarではなくletになります。targetオプションでは変換後にどのバージョンのECMAScriptにするか指定することができます。es2016はletとconstをサポートしているのでletがvarに変換されることはありません。


{
  "compilerOptions": {
    /* Visit https://aka.ms/tsconfig.json to read more about this file */

    /* Projects */
    // "incremental": true,                              /* Enable incremental compilation */
    // "composite": true,                                /* Enable constraints that allow a TypeScript project to be used with project references. */
    // "tsBuildInfoFile": "./",                          /* Specify the folder for .tsbuildinfo incremental compilation files. */
    // "disableSourceOfProjectReferenceRedirect": true,  /* Disable preferring source files instead of declaration files when referencing composite projects */
    // "disableSolutionSearching": true,                 /* Opt a project out of multi-project reference checking when editing. */
    // "disableReferencedProjectLoad": true,             /* Reduce the number of projects loaded automatically by TypeScript. */

    /* Language and Environment */
    "target": "es2016", 
//略

コンパイル後に作成されるgreeter.jsファイルは下記の通りです。


"use strict";
function greeter(person) {
    return 'Hello, ' + person;
}
let user = [0, 1, 2];
document.body.textContent = greeter(user);

target以外のオプションの設定を行い設定することによってどのような違いがあるか確認してみましょう。

outDirオプション

tscコマンドを実行すると変換されたJavaScriptファイルは変換前のtsファイルと同じフォルダに作成されます。変換後のJavaScriptファイルを別のフォルダに保存したい場合はoutDirの設定を./distに変更することでコンパイルしたファイルがdistの中に保存されるようになります。

他にもtsconfig.jsonファイルにはさまざまなオプションが設定されているのでTypeScriptを使いながら理解していく必要があります。本文書の中で動作確認を行いながらいくつかのオプションを紹介していきます。

基本的な型(タイプ)

先ほどの例で:string(コロンとstring)を使って型アノテーションを行いました。string以外にも型が存在します。TypeScriptではType(型)がメインとなるのでどのような型が存在するのか理解しておく必要があります。

  • string型
  • number型
  • boolean型
  • Array(配列)型
  • null型
  • undefined型
  • any型
  • 関数型
  • オブジェクト型

string型

string型では文字列を指定することができます。


const name: string = 'John Doe';

型アノテーションでstringを設定し数値を設定しようとした場合はエラーになります。


const name: string = 25;

“+”を利用して文字列の結合に片方に数値を指定した場合はエラーにはなりません。


const name: string = 'John Doeは' + 25;

number型

number型では数値を指定することができます。


const age: number = 25;

数値なのでマイナスの値や小数点も指定することができます。


const temp: number = -10.5;
数値の型にはnumber型の他にbigint型があります。大きな整数を扱うことができるのでnumber型の他にbigint型もあるということを覚えておいてください。

boolean型

boolean型では真偽値のtrue(真)とfalse(偽)の2つの値を指定することができます。


const isAdmin: boolean = true;

trueまたはfalseしか入らないので0または1を入れてもエラーになります。

Array(配列)型

配列型では配列を指定することができますが指定方法には2通りの方法があります。

型名[]で指定することができます。下記では配列にstringの型が入ることを指定しています。


const fruits: string[] = ['apple', 'banana'];

もう一つはArray<型名>です。型名を<>で囲んでいますがこれはGenerics(ジェネリクス)と呼ばれる指定方法です。後ほどジェネリクスの説明は行います。


const fruits: Array<string> = ['apple', 'banana'];

型名[]、Array<型名>のどちらを利用しても同じです。好きな記述方法を利用することができます。

配列の型をstringに設定している場合は配列の一部の要素に数値を設定するとエラーになります。


const fruits: string[] = ['apple', 10];

上記のように配列にstringとnumberが含まれている場合はUnion( | )を利用することで下記のように記述することができます。Unionを利用することで配列の要素にはstring型が入ることもあればnumber型が入ることもあるということを意味します。


const fruits: (string | number)[] = ['apple', 10];

タプル型

タプル型は配列の要素数が固定(固定長)されておりそれぞれの要素に入る値に型が設定されている場合に利用することができます。


const fruits:[string,number] = ['apple',10];

配列に入れるを値を逆にするとエラーになります。また要素を増やすことはできません。


const fruits:[string,number] = [10,'apple'];

配列の時に配列の要素に入る値の型をUnionでstringかnumberを設定している場合にはstringかnumberであればよいので入れる順番が逆でも要素を増やすこともできます。


const fruits: (string | number)[] = ['apple', 10];
or
const fruits: (string | number)[] = [10, 'apple'];
or
const fruits: (string | number)[] = [10, 'apple','banana'];

null型

null型ではnullを指定することができます。nullのみ設定することができます。


const value = null;

nullは値がないことを意味します。

null型の使い方ですがstring型ではnullの設定は行うことができませんがnameという変数にstringもしくはnullが入る場合は下記のようにUnion型で利用することができます。


let name: string | null = null;

string型でもnull型でもどちらでも設定できるので文字列を入れることもできます。


let name: string | null = 'John Doe';

undefined型

undefined型ではundefinedを指定することができ、undefinedのみ設定することができます。


const value = undefined;

undefinedはnullとは異なり値がまだ値が割り当てられていないので値がないことを意味します。

undefined型の使い方ですがstring型ではundefinedの設定は行うことができませんがnameという変数にstringもしくはundefinedが入る場合は下記のようにUnion型で利用することができます。undefinedの値を文字列に変更することもできます。


let name: string | undefined = undefined;
name = 'John Doe'
nullとundefinedについてはTypeScriptのオプションにstrictNullChecksがあります。デフォルトではtrueに設定されていますがこの値をfalseに変更するとstringやnumberの型アノテーションを設定してもnullとundefinedを設定することができます。

never型

どんな値も入れることができず決して戻ることのない(例外をスローする関数など)関数の型です。

any型

any型を指定するとどのような型の値でも設定することができます。どのような型でも指定できるのでany型を利用するとTypeScriptで型を利用する意味がなくなってしまいます。一時的に型をanyに設定して後ほど型が確定して後に正しい型に直すといった利用方法があります。


  const name_1: any = undefined;
  const name_2: any = 10;
  const name_3: any = null;
  const name_4: any = ['apple', 'banana'];
  const name_5: any = 'John Doe';

関数型

JavaScriptは関数は変数に設定したり関数の引数に利用したりすることができます。下記のhello関数の例の(name:string) => stringが関数型です。引数にstringの型を取り、戻り値にstringの型を設定しています。


const hello: (name: string) => string = (name: string): string => {
  return 'Hello ' + name;
};

上記のhello関数ではreturnで文字列を返すため戻り値にstring型を設定していますが戻り値がないconsole.logを利用した場合にはvoid型を設定することができます。戻り値がない場合に指定することができます。


const hello: (name: string) => void = (name: string): void => {
  console.log('Hello ' + name);
};

オブジェクト型

オブジェクトはプロパティ(キー)と値によって構成されますがプロパティに対して型を設定することができます。


const user: {
  id: number;
  name: string;
} = {
  id: 100,
  name: 'John Doe',
};

userの横の:(コロン)に設定されているのがオブジェクトの型アノテーションです。オブジェクトのプロパティidにnumber型、nameにstring型を設定しています。型の横にセミコロンがついていますがこれは必須ではありませんが下記のように1行で横に型を記述する場合にはセミコロンが必須になります。


const user: { id: number; name: string } = {
  id: 100,
  name: 'John Doe',
};

プロパティ修飾子

オブジェクト型のプロパティにはプロパティ修飾子としてオプショナルプロパティreadonlyプロパティを設定することができます。

オブジェクトの中にはプロパティが存在しない場合もあります。そのような場合にオプショナルプロパティの設定を行うことができます。設定を行う際にはプロパティの名前の横に?をつけます。下記の例ではnameの横に?をつけたのでnameプロパティについてはオプショナルとなり省略することができます。


const user: { id: number; name?: string } = {
  id: 100,
  // name: 'John Doe',
};

nameがオプショナルなので値を設定していない場合にuser.nameにアクセスすると”undefined”が戻されます。

readonlyプロパティを設定するとオブジェクトのプロパティの値を更新できなくなります。設定方法はプロパティ名の前にreadonlyをつけます。


const user: {
  id: number;
  readonly name: string;
  // name: string;
} = {
  id: 100,
  name: 'John Doe',
};

readonlyを設定したnameを更新しようとした場合は”error TS2540: Cannot assign to ‘name’ because it is a read-only property.”のエラーが発生します。オブジェクトのプロパティの更新を誤って行いたくない場合はreadonlyを設定することでそのようなミスを防ぐことができます。

Interface(インターフェイス)とは

TypeScriptのInterfaceはオブジェクトの形を決める設計図のようなものでオブジェクトやClassを構成するプロパティの名前と型を定義することができます。プロパティや関数を定義するのみで実際の値や処理は記述されていません。Interfaceを利用する際にContract(契約)という言葉が利用されます。契約というのがInterfaceの特徴をよく表しており契約なのでその契約を遵守する必要がありInterface通りに従って設定を行う必要があります。

オブジェクト型では直接オブジェクトに型を設定していましたがInterfaceを利用して型アノテーションを行うことができます。

Interfaceの使い方

Interfaceを使ってPersonを作成します。InterfaceのPersonはfirstNameとlastNameの2つのプロパティを持ち、どちらの型もstringを持ちます。Interfaceはプロパティ名とその型を設定しただけで値は持たないことがわかります。


interface Person {
    firstName: string;
    lastName: string;
}

greeter関数の引数にオブジェクトを渡したい場合、オブジェクトの型アノテーションに作成したインターフェイスを設定することができます。


function greeter(person: Person) {
  return "Hello, " + person.firstName + " " + person.lastName;
}

Interfaceを利用しない場合には下記のように記述する必要があります。


function greeter(person: { firstName: string; lastName: string }) {
  return 'Hello, ' + person.firstName + ' ' + person.lastName;
}

引数をInterface Personで型アノテーションを指定するとgreeter関数を実行する際に挿入するオブジェクトはfirstNameとlastNameを持ちその型はstringである必要があります。


let user = { firstName: "Jane", lastName: "Doe" };

document.body.textContent = greeter(user);

ここまでのコードをまとめてコンパイルを実行してindex.htmlでgreeter.jsファイルを読み込むと画面上には”Hello Jane Doe”が表示されます。


interface Person {
  firstName: string;
  lastName: string;
}

function greeter(person: Person) {
  return "Hello, " + person.firstName + " " + person.lastName;
}

let user = { firstName: "Jane", lastName: "Doe" };

document.body.textContent = greeter(user);

index.htmlファイルに以下のコードを利用しています。


<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <script src="./greeter.js"></script>
  </body>
</html>

greeter関数の引数に設定したinterface Personの型アノテーションが本当に適用されているのか確認するためにuser変数に設定するプロパティをfirstNameからfullNameに変更してみましょう。


interface Person {
  firstName: string;
  lastName: string;
}

function greeter(person: Person) {
  return "Hello, " + person.firstName + " " + person.lastName;
}

let user = { fullName: "Jane", lastName: "Doe" };

document.body.textContent = greeter(user);

コンパイルを実行するとプロパティのfirstNameがないとエラーが表示されます。 コンパイルを実行しなくてもエディターにもエラーメッセージが表示されます。プロパティ名がInterfaceのPersonの定義通りに含まれているのかチェックされていることがわかります。


$ tsc greeter.ts 
greeter.ts:12:37 - error TS2345: Argument of type '{ fullName: string; lastName: string; }' is not assignable to parameter of type 'Person'.
  Property 'firstName' is missing in type '{ fullName: string; lastName: string; }' but required in type 'Person'.

次にプロパティ名ではなくlastNameの値を文字列から数値に変更してみましょう。InterfaceのPersonではlastNameの型はstringに設定されています。


let user = { firstName: "Jane", lastName: 20 };

コンパイルを実行すると先程のプロパティ名を変更した時と異なるエラーが表示されます。lastNameの値がnumberなのでPersonのlastNameで定義した型のstringではないというエラーになっています。ここでもInterfaceのPersonの定義通り設定が行われているかチェックされていることがわかります。


 $ tsc greeter.ts 
greeter.ts:13:37 - error TS2345: Argument of type '{ firstName: string; lastName: number; }' is not assignable to parameter of type 'Person'.
  Types of property 'lastName' are incompatible.
    Type 'number' is not assignable to type 'string'.

変数userを設定する場合にも型アノテーションとして設定したInterfaceのPersonを利用することができます。


let user: Person = {
    firstName: "John",
    lastName: "Smith"
}

interfaceのPersonに存在しないプロパティを追加しようとするとエラーになります。


interface Person {
  firstName: string;
  lastName: string;
}

let user: Person = {
    firstName: "John",
    lastName: "Smith",
    age: 25
}

またuserを定義した後に存在しないプロパティに値を設定しようとするとエラー(Property ‘age’ does not exist on type ‘Person’.)になります。


interface Person {
  firstName: string;
  lastName: string;
}

let user: Person = {
  firstName: "John",
  lastName: "Smith",
}

user.age = 10

このようにInterfaceは設計図のような役割を持ちその設計図通りではない設定を行った場合にはエラーとなります。一度定義したInterfaceはexportを利用することでいろいろな場所で再利用できます。また名前をつけることでInterfaceを利用するデータがどのような意味を持っているかもわかるようになります。

Interfaceに関数を含む場合

オブジェクトのプロパティには関数を設定することができます。Interfaceでオブジェクトの関数の型を設定する場合は下記のように記述することができます。


interface Person {
  firstName: string;
  lastName: string;
  greeting: (message: string) => string;
}

let user: Person = {
  firstName: 'John',
  lastName: 'Smith',
  greeting(message) {
    return `${message} ${this.firstName}`;
  },
};

console.log(user.greeting('Hello'));
// Hello John

Interfaceに含まれる関数は下記のように記述することもできます。


interface Person {
  firstName: string;
  lastName: string;
  greeting(message: string): string;
}

TypeScriptでのClassの作成

TypeScriptでClassを利用する際は下記のように記述することができます。


class Person {
  firstName: string;
  lastName: string;
  constructor(firstName: string, lastName: string) {
    this.firstName = firstName;
    this.lastName = lastName;
  }
  fullName(): string {
    return `${this.firstName} ${this.lastName}`;
  }
}

const user = new Person('John', 'Doe');

console.log(user.fullName());
// John Doe

アクセス修飾子の理解

Classにはプロパティやメソッドにpublic、private、protected、readonlyというアクセス修飾子を設定することができアクセスできる範囲を制限することができます。

デフォルトでは明示しなければpublic修飾子となり外部からアクセス、値を設定することができます。先ほどの例であればfirstNameもlastNameも外部から更新することができます。


class Person {
  firstName: string;
  lastName: string;
  constructor(firstName: string, lastName: string) {
    this.firstName = firstName;
    this.lastName = lastName;
  }
  fullName(): string {
    return `${this.firstName} ${this.lastName}`;
  }
}

const user = new Person('John', 'Doe');

user.firstName = 'Jane';

console.log(user.fullName());
// Jane Doe

外部からアクセスできないprivate修飾子をfirstNameに設定します。


class Person {
  private firstName: string;
  lastName: string;
  constructor(firstName: string, lastName: string) {
    this.firstName = firstName;
    this.lastName = lastName;
  }
  fullName(): string {
    return `${this.firstName} ${this.lastName}`;
  }
}

const user = new Person('John', 'Doe');

user.firstName = 'Jane';

firstNameを変更しようとすると”error TS2341: Property ‘firstName’ is private and only accessible within class ‘Person’.”メッセージが表示され更新することはできなくなります。メソッドにもprivate修飾子を設定することができ設定するとエラーメッセージが表示されます。

次にprotected修飾子の確認を行います。protected修飾子を設定することで継承したClassからのみアクセスすることができます。private修飾子では継承したClassではアクセスすることができません。public修飾子は継承したClassからアクセスすることができますが外部からもアクセスして更新することできます。


class Person {
  protected firstName: string;
  lastName: string;
  constructor(firstName: string, lastName: string) {
    this.firstName = firstName;
    this.lastName = lastName;
  }
  fullName(): string {
    return `${this.firstName} ${this.lastName}`;
  }
}

class User extends Person {
  isAdmin: boolean;
  constructor(firstName: string, lastName: string, isAdmin: boolean) {
    super(firstName, lastName);
    this.isAdmin = isAdmin;
  }
  fullName(): string {
    return super.fullName();
  }
  yourFirstName(): void {
    console.log(this.firstName);
  }
}

const user = new User('John', 'Doe', false);

user.yourFirstName();
//John

ClassのPersonを定義していますがfirstNameをPersonを継承するUserクラスでもアクセスできるようにprotected修飾子をつけています。UserクラスのyourfirstNameメソッドを実行するとfirstNameにアクセスすることがでJohnが表示されます。

protectedからprivateに変えるとPersonクラスからしかアクセスすることができないので”Property ‘firstName’ is private and only accessible within class ‘Person’.ts(2341)”のエラーが表示されます。デフォルトのpublicに設定した場合はJohnを表示されますが外部からアクセスから可能なので変更することができます。


const user = new User('John', 'Doe', false);

user.firstName = 'Jane';

user.yourFirstName();
//Jane

protectedに設定して上記を実行すると”Property ‘firstName’ is protected and only accessible within class ‘Person’ and its subclasses.”のエラーメッセージが表示されます。

修飾子のreadonlyを設定するとconstructorの初期化ではreadonlyを設定したプロパティの値を更新することができますがメソッドでアクセスは可能ですが更新を行うことができません。


class Person {
  readonly firstName: string;
  lastName: string;
  constructor(firstName: string, lastName: string) {
    this.firstName = firstName;
    this.lastName = lastName;
  }
  fullName(): string {
    return `${this.firstName} ${this.lastName}`;
  }
  setName(name: string) {
    this.firstName = name;
  }
}

const user = new Person('John', 'Doe');
user.setName('Jane');

console.log(user.fullName());

上記のコードではfirstNameにreadonly修飾子が設定されているのでsetNameメソッドで上書きしようとすると”Cannot assign to ‘firstName’ because it is a read-only property.ts(2540)”のエラーメッセージが表示されます。public, private, protected修飾子が設定されている場合はエラーメッセージが表示されることはありません。更新が必要でない場合はreadonlyを利用することができprivate修飾子と一緒に利用することができます。その場合はClassの外側からもアクセスすることはできません。

Classの別の記述方法

先ほど確認した修飾子を利用することでClassの記述方法を簡略化することができます。publicの修飾子を削除すると下記のコードは動作しません。


class Person {
  constructor(public firstName: string, public lastName: string) {}
  fullName(): string {
    return `${this.firstName} ${this.lastName}`;
  }
}

const user = new Person('John', 'Doe');

console.log(user.fullName());
//John Doe

TypeScriptのコードからの変換後のJavaScriptファイルの中身を確認すると通常の記述方法に変換されていることがわかります。


"use strict";
class Person {
    constructor(firstName, lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }
    fullName() {
        return `${this.firstName} ${this.lastName}`;
    }
}
const user = new Person('John', 'Doe');
console.log(user.fullName());

ClassにInterfaceを利用する

Classを作成する際にInterfaceを利用することができます。

作成したInterfaceを利用してClassを作成したい場合はimplementsを使って記述することができます。

Interfaceは先程設計図のような役割といったとおりInterfaceをimplementsしたクラスStudentはfirstNameとlastNameを持つ必要があります。


interface Person {
  firstName: string;
  lastName: string;
}

class Student implements Person {
  public firstName: string;
  public lastName: string;
  constructor(firstName: string, lastName: string) {
    this.firstName = firstName;
    this.lastName = lastName;
  }
}

// public修飾子がなくてもOK
class Student implements Person {
  firstName: string;
  lastName: string;
  constructor(firstName: string, lastName: string) {
    this.firstName = firstName;
    this.lastName = lastName;
  }
}

StudentクラスにfirstNameもしくはlastNameがない場合はエラーメッセージが表示されます。Interfaceではアクセス修飾子を設定することができないのでclassでpublic修飾子ではなくprivate修飾子を指定した場合も確認しておきましょう。


class Student implements Person {
  private firstName: string;
  public lastName: string;
//略

以下のようにprivateに設定していることでエラーメッセージが表示されます。


Class 'Student' incorrectly implements interface 'Person'.
  Property 'firstName' is private in type 'Student' but not in type 'Person'.ts(2420)

public, private, protectedのアクセス修飾子と異なりInterfaceにreadonlyを設定してもエラーになることはありませんが下記のようにreadonlyを設定してもClassのfirstNameプロパティの更新ができないわけではありません。


interface Person {
  readonly firstName: string;
  lastName: string;
}

class Student implements Person {
  firstName: string;
  public lastName: string;
  constructor(firstName: string, lastName: string) {
    this.firstName = firstName;
    this.lastName = lastName;
  }
}

const user = new Student('John', 'Doe');

user.firstName = 'Jane';

console.log(user.firstName);

readonlyを設定したい場合はClass側で設定を行う必要があります。設定すると”Cannot assign to ‘firstName’ because it is a read-only property.ts(2540)”エラーメッセージが表示されます。


interface Person {
  readonly firstName: string;
  lastName: string;
}

class Student implements Person {
  readonly firstName: string;
  public lastName: string;
  constructor(firstName: string, lastName: string) {
    this.firstName = firstName;
    this.lastName = lastName;
  }
}

const user = new Student('John', 'Doe');

user.firstName = 'Jane';

console.log(user.firstName);

地域によっては名前にmiddleNameを使う場合もあります。その場合に備えmiddleNameを追加したInterfaceを作成します。追加した場合はInterface Personをimplementsする際にはプロパティにmiddleNameが必要となります。


interface Person {
  firstName: string;
  middleName: string;
  lastName: string;
  fullName(): void;
}

class Student implements Person {
  firstName: string;
  middleName: string;
  lastName: string;
  constructor(firstName: string, middleName: string, lastName: string) {
    this.firstName = firstName;
    this.middleName = middleName;
    this.lastName = lastName;
  }
  fullName() {
    return `${this.firstName} ${this.middleName} ${this.lastName}`;
  }
}

const user = new Student('John', 'M', 'Doe');

console.log(user.fullName());
// John M Doe
もしStudentクラスにmiddleNameがなかった場合は、コンパイル時にerror TS2420: Class ‘Student’ incorrectly implements interface ‘Person’.
Property ‘middleName’ is missing in type ‘Student’ but required in type ‘Person’.のエラーが表示されます。

middleNameを使わない地域によってはmiddleNameはオプションなので必要ではありません。必須ではない(オプション)プロパティに”?”をつけるとStudentクラスでmiddleNameがなくてもエラーになりません。


interface Person {
  firstName: string;
  middleName?: string;
  lastName: string;
  fullName(): void;
}

class Student implements Person {
  firstName: string;
  lastName: string;
  constructor(firstName: string, lastName: string) {
    this.firstName = firstName;
    this.lastName = lastName;
  }
  fullName() {
    return `${this.firstName} ${this.lastName}`;
  }
}

const user = new Student('John', 'Doe');

console.log(user.fullName());
//John Doe

Interfaceに定義されているプロパティやメソッドは必ず設定をしなければなりませんがinterfaceに存在しないプロパティやメソッドをClassに追加することはできます。InterfaceからlastNameプロパティとfullNameメソッドを削除していますがClassにはlastNameプロパティもfullNameメソッドも存在します。


interface Person {
  firstName: string;
  middleName?: string;
}

class Student implements Person {
  firstName: string;
  lastName: string;
  constructor(firstName: string, lastName: string) {
    this.firstName = firstName;
    this.lastName = lastName;
  }
  fullName() {
    return `${this.firstName} ${this.lastName}`;
  }
}

const user = new Student('John', 'Doe');

console.log(user.fullName());
//John Doe

しかしInterfaceで定義されているプロパティ、メソッドをClassで記述していない場合はエラーメッセージが表示されます。

複数のInterfaceを実装することができます。またconstructorで設定するプロパティにデフォルト値を設定することができます。InterfaceのPersonとUserと作成して Studentクラスで実装しています。isAdminプロパティの初期値をfalseに設定しているのでクラス作成時(new Student)にisAdminの引数を設定しなくても自動でfalseが設定されます。


interface Person {
  firstName: string;
  lastName: string;
}

interface User {
  isAdmin: boolean;
}

class Student implements Person, User {
  firstName: string;
  lastName: string;
  isAdmin: boolean;
  constructor(firstName: string, lastName: string, isAdmin: boolean = false) {
    this.firstName = firstName;
    this.lastName = lastName;
    this.isAdmin = isAdmin;
  }
}

const user = new Student('John', 'Doe');

console.log(user.isAdmin);
//false

Type Alias(型エイリアス )とは

Interfaceと同様にType Alias(型エイリアス )を利用することで型に別名をつけることができます。型エイリアスで定義したものは一箇所ではなく複数箇所で使い回すことができます。

エイリアスとは別名という意味があるので型エイリアスを利用することでstring型にNameという別名の型を設定することができます。


type Name = string;

型エイリアスでNameを設定したのでstring型やnumber型と同じようにNameを型として型アノテーションとして利用することができます。


function hello(name: Name) {
  return 'Hello, ' + name;
}

hello関数の引数に数値を入れようとした場合はエラーになります。

typeでNameを定義した時に”=”を利用しているので変数のように後から自由に型を上書きすることはできません。


type Name = string;

Name = number; // 'Name' only refers to a type, but is being used as a value here.

型エイリアスはオブジェクトにも別名をつけることができます。


type Person = {
  firstName: string;
  lastName: string;
};

interfaceで利用したgreeter関数の引数personの型アノテーションに型エイリアスを利用して定義したPersonを設定してもInterfaceと同様に型アノテーションとして動作します。


type Person = {
  firstName: string;
  lastName: string;
};

function greeter(person: Person) {
  return 'Hello, ' + person.firstName + ' ' + person.lastName;
}

TypeとInterfaceの違い

オブジェクトの型では型エイリアスでもInterfaceでも同じように動作することがわかりました。2つの構文を記述して違いを確認してみましょう。


type Person = {
  firstName: string;
  lastName: string;
};

//Interfaceの場合
interface Person {
    firstName: string;
    lastName: string;
}

記述方法を見るとtypeでは”=”を使っていますがinterfaceでは”=”は使っていないという違いがあります。またtypeの最後にはセミコロンをつける必要がありますがinterfaceでは最後にセミコロンをつける必要がありません。各プロパティのセミコロンについてはプロパティ毎に改行している場合はセミコロンをつけるかどうかは任意です。

記述方法以外の違いについても確認しておきます。

Interfaceはオブジェクトやクラスの型定義に利用することができますが型エイリアスはそれ以外にもリテラルのunion型などに別名をつけることができます。リテラル型のUnionは下記のような形です。


type fruits = 'apple' | 'banana' | 'lemon';

Interfaceは同じ名前で定義を行うことができプロパティに設定した型を追加することができます。下記のようなことが可能になります。別々のプロパティに型をした同じ名前のinterfaceを作成すると2つの設定が反映されinterfaceのPersonにはfirstNameとlastNameを設定したことになります。これを”宣言のマージ”と呼びます。マージ(merge)には合併するという意味があります。


interface Person {
  firstName: string;
}

interface Person {
 lastName: string;
}

const user: Person = {
  firstName: 'John',
  lastName: 'Doe'
}

同じinterface名に同じ名前のプロパティが入っていてもマージが行われます。


interface Person {
  firstName: string;
  age: number;
}

interface Person {
  lastName: string;
  age: number;
}

const user: Person = {
  firstName: 'John',
  lastName: 'Doe',
  age: 10,
};

同じプロパティ名で型が異なるとエラーになります。(Subsequent property declarations must have the same type. Property ‘age’ must be of type ‘string’, but here has type ‘number’.ts(2717))


interface Person {
  firstName: string;
  age: string;
}

interface Person {
  lastName: string;
  age: number;
}

型エイリアスでは上記のように同じ名前で設定を行っても追加されることはなくエラーメッセージで”Duplicate identifier ‘Person'”と表示され利用することはできません。


type Person = {
  firstName: string;
};

type Person = {
  lastName: string;
};

const user: Person = {
  firstName: 'John',
  lastName: 'Doe',
};

型エイリアスではIntersection型(交差型)の”&”を利用することで同様の設定を行うことができます。


type PersonFirst = {
  firstName: string;
};

type PersonLast = {
  lastName: string;
};

type Person = PersonFirst & PersonLast;

const user: Person = {
  firstName: 'John',
  lastName: 'Doe',
};

Interfaceでは”宣言のマージ”以外にもextedsを利用して型を組み合わせことができます。


interface PersonFirst {
  firstName: string;
}

interface PersonLast {
  lastName: string;
}

interface Person extends PersonFirst, PersonLast {}

const user: Person = {
  firstName: 'John',
  lastName: 'Doe',
};

型エイリアスではリテラル型のUnionを設定することができます。設定した文字列のいずれかを設定することができます。apple, banana, lemon以外の値を設定するとエラーになります。Interfaceでは下記を定義することはできません。


type fruits = 'apple' | 'banana' | 'lemon';

const fruit: fruits = 'apple';

関数型については型エイリアスだけではなくInterfaceを利用して記述することもできます。

型エイリアスの場合は下記のように記述することができます。


type helloFunc = (name: string) => void;

const hello: helloFunc = (name: string): void => {
  console.log('Hello ' + name);
};

Interfaceの場合は下記のように記述することができます。


interface helloFunc {
  (name: string): void;
}

const hello: helloFunc = (name: string): void => {
  console.log('Hello ' + name);
};

UnionとIntersection

Union(合併)とIntersection(交差)を利用した例についてはここまでで何度か登場しましたが改めて説明を行います。

Unionを下記のように設定することで変数のvalueはstring型, number型どちらの値でも設定することができます。下記では2つですが、2つ以上の型を設定することができます。その場合はどちらかではなくいずれかの型で値を設定することが可能になります。Unionの場合は”|”をつないで定義することができます。


let value: string | number = 100;
value = 'John';

Typeエイリアスとオブジェクトを利用してUnionを確認します。PersonFirstとPersonLastが共通に持つageは必ず設定する必要がありますがそれ以外のプロパティについては必須ではありません。


type PersonFirst = {
  firstName: string;
  age: number;
  height: number;
};

type PersonLast = {
  lastName: string;
  age: number;
  weight: number;
};

type Person = PersonFirst | PersonLast;

const user: Person = {
  firstName: 'John',
  lastName: 'Doe',
  age: 10,
  weight: 60,
};

IntersectionではTypeエイリアスで確認したように指定したすべての型を設定する必要があるので上記の例を利用するとPersonFirstとPersonLastで設定したプロパティをすべて設定する必要があります。どれか一つ欠けてもエラーになります。Intersectionの場合は”&”をつないで定義することができます。


type PersonFirst = {
  firstName: string;
  age: number;
  height: number;
};

type PersonLast = {
  lastName: string;
  age: number;
  weight: number;
};

type Person = PersonFirst & PersonLast;

const user: Person = {
  firstName: 'John',
  lastName: 'Doe',
  age: 10,
  weight: 60,
  height:170,
};

intersectionを利用して型エイリアスとInterfaceを利用して別の型エイリアスを定義することもできます。


type PersonFirst = {
  firstName: string;
  age: number;
  height: number;
};

interface PersonLast {
  lastName: string;
  age: number;
  weight: number;
}

type Person = PersonFirst & PersonLast;

const user: Person = {
  firstName: 'John',
  lastName: 'Doe',
  age: 10,
  weight: 60,
  height: 170,
};

ジェネリクス

ジェネリクスという名前から何を表しているのかイメージするのが難しい上、T、Uや<>などの記号の羅列でTypeScriptを学習し始めた人が最初に戸惑う機能の一つです。

ジェネリクスは関数であれば引数、戻り値の型の指定を関数を定義した時に行うのではなく(TやUなどの仮の型名を設定しておく)、関数を利用する際に型の指定(仮の型名TやUを実際に存在するstring型やnumber型に置き換える)を行うことができる機能です。関数にジェネリクスを利用することで複数の型に対応できる汎用的な関数を定義することができます。

上記の説明も具体例がないと難しいかもしれませんが下記の例を読むとすっきりすると思います。

ジェネリクスを理解するための例

ジェネリスクを説明する際によく利用される下記の2つの関数を使ってジェネリクスを説明していきます。


// for number type
function fun(args: number): number {
  return args;
}

// for string type
function fun(args: string): string {
  return args;
}

関数名が同じということを置いておいて2つの関数を眺めると共通点が見られます。引数と戻り値の型が2つの関数でstring型とnumber型と異なりますがどちらも引数と戻り値で同じ型の設定を行っています。

上記の関数の形からany型を利用することで2つ関数の定義を1つの定義で記述することができるようになります。any型の場合はどのような型も設定できるのでnumber型を設定してもstring型を戻すということも可能なのでany型を利用するのは勧められません。


// for any type
function fun(args: any): any {
  return args;
}

そこでジェネリクスの登場です。ジェネリクスを利用して先ほどの関数の定義を記述すると下記のように記述することできます。関数名の横に<T>を追加します。<>の中に入れる文字列はTは仮の型名でTとしていますが任意の名前をつけれることができますが慣例的にTと設定します。引数と戻り値は同じ型なので仮の型名のTで設定することができます。


function fun<T>(args:T):T {
  return args;
}

//アロー関数で記述した場合
const fun = <T>(args: T): T => args;

定義時は<>の中は仮の型名のTを設定していますが関数を利用する時に<>の中に実際に存在する型を指定することで指定した型で関数を実行することができます。関数を定義した時に型を指定するのではなく利用する時に型を指定することができます。定義時に仮の型名Tを設定していた引数、戻り値も指定した型として実行されます。ジェネリックを利用することで定義した1つの関数を利用して下記のように型の異なる処理を実行ができるようになります。


let result = fun<string>("Hello World");
let result2 = fun<number>(200);

ジェネリクスと型推論により下記のように<>を省略して記述することもできます。


let result = fun('Hello World');
let result2 = fun(200);

さらにTに設定できる型はstring型とnumber型に制限されているわけではないのでオブジェクト型を利用することもできます。


let result3= fun<{ name: string }>({ name: 'John Doe' });

複数の型を設定したい場合は下記のように設定を行うことができます。


function funs(arg1: T, arg2: U): [T, U] {
  return [arg1, arg2];
}
let result4 = funs('Hello', 100);
console.log(result4);
//結果
[ 'Hello', 100 ]

オブジェクト型の場合はInterfaceや型エイリアスのどちらかを利用して書き換えることもできます。


//Interface
interface User {
  name: string;
}

//型エイリアス
type User = {
  name: string;
};

let result3 = fun<User>({ name: 'John Doe' });

Tで指定できる型に制限を加えたい場合にはextendsを利用することができます。下記ではstringとnumberだけTに型指定できるように制限を加えています。


const fun = <T extends string | number>(args: T): T => {
  return args;
};

Tにnumberとstringを設定した場合は問題がありませんがオブジェクトを設定した場合は”Type ‘User’ does not satisfy the constraint ‘string | number’.”のエラーメッセージが表示されます。

関数以外にもジェネリクスを利用することができるのでいくつかの例を使いながら説明していきます。

Interfaceでジェネクリクス

Interfaceを使ってkey, valueプロパティを持つKeyPairを定義します。ジェネリクスでは複数の型を設定することができます。Tという型の仮名を使いましが2つ利用する場合にはTの次はUを利用します。慣例なので好きな名前を設定することは可能です。


interface KeyPair<T, U> {
  key: T;
  value: U;
}

TとUの型を設定時に指定することができます。Interfaceを使ってオブジェクトは以下のように設定することができます。keyやstringは自由な型を設定することができます。


const kv1: KeyPair<number, string> = { key: 1, value: 'Steve' };
const kv2: KeyPair<number, number> = { key: 1, value: 1000 };
const kv3: KeyPair<string, string[]> = { key: '10', value: ['John','Steve','Jane'] };

型エイリアスでジェネリクス

型エイリアスを利用してジェネリクスを設定することも可能です。Interfaceの例を型エイリアスでも利用すると下記のように記述することができます。


type KeyPair<T, U> = {
  key: T;
  value: U;
};

const kv1: KeyPair<number, string> = { key: 1, value: 'Steve' };
const kv2: KeyPair<number, number> = { key: 1, value: 1000 };

const kv3: KeyPair<string, string[]> = {
  key: '10',
  value: ['John', 'Steve', 'Jane'],
};

最初に利用した関数をtypeを利用して設定することもできます。

型推論(Type Inference)

TypeScriptでは型アノテーションによって型を明示的に指定しなくてもTypeScriptが型を推論してくれる機能を持っています。例えば下記のように変数を定義する際に型アノテーションで型を指定しなくてもエラーが表示されることはありません。


let greeting = 'Hello';
//型アノテーションで型を指定した場合
let greeting:string = 'Hello';

型推論によってgreentingはstring型とみなされます。この後にgreetingに数値を入れようとするとエラーメッセージ”Type ‘number’ is not assignable to type ‘string’.”が表示されます。

Visual Studio Codeを利用している場合には変数にカーソルを当てると型の情報を確認することができます。

VSCodeによる型情報の確認
VSCodeによる型情報の確認

string型以外にもnumber型、boolean型、配列型でも型推論は行われます。型アノテーションで型を指定しなくても利用することができます。


let age = 20;
let isAmin = true;
let fruits = ['apple', 'banana', 'lemon'];

上記のfruitsはstring[]型として推論されるのでStringが持つtoUpperCaseメソッドを実行することはできます。

しかしfruitsの配列の一部を数値に変更するとfruitsはstring | number[]型と推論されnumber型の要素にUpperCaseメソッドを実行しようとするとエラー(“Property ‘toUpperCase’ does not exist on type ‘string | number’.Property ‘toUpperCase’ does not exist on type ‘number'”)になります。


let fruits = [100, 'banana', 'lemon'];
fruits[0].toUpperCase(); //

関数型を確認した場合に以下のように定義しました。コードを見ると型の設定で同じ記述を2回行っています。


const hello: (name: string) => void = (name: string): void => {
  console.log('Hello ' + name);
};

型推論が働くのではないかと思いすべての型アノテーションを設定することはできません。エラーとなります。”error TS7006: Parameter ‘name’ implicitly has an ‘any’ type.”


const hello = (name) => {
  console.log('Hello' + name);
};

上記のように記述することはできませんが下記のように記述することができます。この場合はhelloの型が(name:string):voidだと推論されます。


const hello = (name: string): void => {
  console.log('Hello ' + name);
};

hello関数では戻り値にvoid型を設定していますが戻りの型アノテーションで明示的に型を指定しなくても型推論してくれるので省略することができます。


const hello = (name: string) => {
  console.log('Hello ' + name);
};

関数の場合は特に明確になりましたがTypeScriptが持つ型推論を活用することで冗長に型の定義を記述する必要がなくなりコードの可読性が上がりコード量も減らすことができます。

型アサーション

TypeScriptが推論する型に対して明示的に型を設定することで推論した型の情報を上書きすることができます。

下記のgetValue関数は引数にboolean型のformatを取ることができ処理の中で分岐を行っているので戻り値はstringの場合とnumberの場合があります。


const getValue = (format: boolean): string | number => {
  return format ? '10' : 10;
};

getValueの引数にtrueを設定した場合は文字列の’10’が戻されるのですがvalueにはstring型かnumber型のどちらかが入ることになるので”Property ‘length’ does not exist on type ‘string | number’.
Property ‘length’ does not exist on type ‘number’とエラーメッセージが表示されます。


const value = getValue(true);
const digit = value.length;

これを型アサーションのas 型を設定することで指定した型で上書きすることができエラーが解消されます。


const value = getValue(true) as string;
const digit = value.length;

getValueの設定の戻り値にnumber型かstring型が設定されているのでstring型やnumber型を設定することはできますが全く関係のないboolean型などを利用することはできません。上書きを行いたい型が元の型の一部の場合のみ利用できます。

型アサーションの記述方法にはもう一つあり<>の中に型を入れることで設定を行うことができます。


const value = <string>getValue(true);

typeof演算子の使い方

JavaScriptではtypeof演算子を利用することで変数やオブジェクトのプロパティが持つ型を取得することができます。Johnという文字列を設定した変数firstNameにtypeof演算子を利用するとfirstNameに設定されている型のstringを取得することができます。


let firstName = 'John';
let price = 20;
let user = {
  firstName: 'John',
  lastName: 'Doe',
};

console.log(typeof firstName);
console.log(typeof 'Jane');
console.log(typeof price);
console.log(typeof user);
// 実行結果
string
string
number
object

上記の例からtypeofを利用することで変数から型を取り出せることがわかります。取り出した型を型エイリアスで設定を行うことができます。Nameのstring型、PriceはNumber型、Userには{firstName:string, lastName:string}の型が設定されます。


let firstName = 'John';
let price = 20;
const user = {
  firstName: 'John',
  lastName: 'Doe',
};

type Name = typeof firstName
type Price = typeof price
type User = typeof user;

実際に型エイリアスUserに型の設定が行われているのか確認するために型アノテーションを利用して変数personを定義します。firstNameとlastNameは設定することができますがageを追加するとエラーメッセージが表示されて設定することができません。


const person: User = {
  firstName: 'Jane',
  lastName: 'Doe',
  age:20
}

またas constを利用することでtypeof演算子で取得できる型がstring型からリテラル型に変わります。


const user = {
  firstName: 'John',
  lastName: 'Doe',
} as const;

type User = typeof user;
// 結果
type User = {
    readonly firstName: "John";
    readonly lastName: "Doe";
}

配列にtypeof演算子を利用します。


const fruits = ['apple', 'banana', 'lemon'];
type Fruit = typeof fruits;
//結果
string[]

const fruits = [1, 100, 2];
type Fruit = typeof fruits;
//結果number[]

const fruits = [100, 'banana', 'lemon'];
type Fruit = typeof fruits;
//結果
(string|number)[]

as contとtypeofを利用することで配列に含まれる要素のUnion型を設定することができます。


const fruits = ['apple', 'banana', 'lemon'] as const;
type Fruit = typeof fruits[number];

// type Fruit = 'apple' | 'banana' | 'lemon'

keyof 演算子

keyof演算子を利用することで型エイリアス、Interfaceで設定したプロパティ名をUnion型として取得することができます。下記の例では型エイリアスのPersonにkeyof演算子を利用すると”name” | “age”のUnion型を取得することができます。型エイリアスのUserには’name’ | ‘age’のリテラルのUnion型が設定されます。Userを型アノテーションに利用した場合は”name”と”age”のみ設定することができます。


type Person = {
  name: string;
  age: number;
};

type User = keyof Person;

const user: User = 'name';
const user2: User = 'age';

const user3: User = 'email'; // Error

as const

先ほど出てきたas constについてさらに動作確認を行っていきます。

オブジェクトにas constを設定するとすべてのプロパティがreadonlyになるため更新ができなくなります。


const user = {
  firstName: 'John',
  lastName: 'Doe',
} as const;

user.firstName = 'Jane' //Error

addressプロパティを追加してaddressプロパティをオブジェクト化してもas constを利用するとプロパティprefectureもreadonlyになるため更新できません。


const user = {
  firstName: 'John',
  lastName: 'Doe',
  address: {
    prefecture: 'Tokyo',
  },
} as const;

user.address.prefecture = 'Chiba' //Error

//
const user: {
    readonly firstName: "John";
    readonly lastName: "Doe";
    readonly address: {
        readonly prefecture: "Tokyo";
    };
}

配列を利用してas constの動作確認を行います。

配列を設定した場合はpushメソッドで配列の要素を追加することができます。fruitsの型はstring[]と推論されます。


const fruits = ['apple', 'banana', 'lemon'];

fruits.push('meron');

//型
string[]

as constを設定した場合は要素の追加を行おうとするとエラーが表示されます。


const fruits = ['apple', 'banana', 'lemon'] as const;

fruites.push('meron');

//型
const fruits: readonly ["apple", "banana", "lemon"]

型定義ファイル

型定義ファイルを作成したい場合はtscコマンドに-dオプションをつけることで作成することができます。

greeting.tsファイルを作成してその中にgreeting関数を追加します。引数にstring型を設定したシンプルな関数です。


const greeting = (name: string) => {
  console.log('Hello' + name);
};

tscコマンドに-dオプションをつけて実行します。


 % tsc -d 

実行するとgreeting.d.tsファイルが作成されます。中身を見ると型定義ファイルに型が記述されていることがわかります。greeting関数では戻り値の型を指定していませんでしたが型定義ファイルでは戻り値のvoidが設定されていることが確認できます。


declare const greeting: (name: string) => void;

インデックスシグネチャ

Interfaceや型エイリアスでオブジェクトを定義する際にオブジェクトに含まれるプロパティ名を設定できるインデックスシグネチャという機能があります。インデックスシグネチャを利用する場合には[key:string]という構文を利用します。keyには任意の名前をつけることができ、そのkeyはstring型になることを表しています。例を使って動作確認を行います。

型エイリアスでUserを定義してインデックスシグネチャを使って定義を行っています。User型を利用する際にstring型のfirstNameとlastNameのプロパティ名を設定することができます。


type User = {
  [key: string]: string;
};

const user: User = {
  firstName: 'John',
  lastName: 'Doe',
};

keyにはどのような名前でも入れることができるのでプロパティを追加することができます。


user.middlename = 'M';
or
user['middlename'] = 'M';

しかしstringではない値を設定するとエラーになります。(Type ‘number’ is not assignable to type ‘string’)


user.age = 24;

keyについては任意の名前をつけることができるので”key”から”K”に変更しても動作は変わりません。


type User = {
  [K: string]: string;
};

interfaceでも利用することができるのでinterfaceで記述すると下記のようになります。


interface User {
  [K: string]: string;
}

keyに設定する型についてはstringまたはnumberを設定することができます。stringからnumberに変更するとfirstNameが文字列なのでエラーになります。


interface User {
  [K: number]: string;
}

const user: User = {
  firstName: 'John',
  lastName: 'Doe',
};

プロパティ名を文字列から数値に変更することでエラーメッセージは解消されます。


interface User {
  [K: number]: string;
}

const user: User = {
  0: 'John',
  100: 'Doe',
};

console.log(user[0])
//結果
John

明示的にプロパティを追加した場合インデックスシグネチャに設定した型に設定を行う必要があります。middleNameを追加することができますがageはnumber型なのでエラーになります。

string型、number型どちらも許可したい場合は下記のように記述することでエラーが解消します。


type User = {
  [key: string]: string | number;
  middleName: string;
  age: number;
};

const user: User = {
  firstName: 'John',
  lastName: 'Doe',
  middleName: 'M',
  age: 10,
};

console.log(user);
//結果
{ firstName: 'John', lastName: 'Doe', middleName: 'M', age: 10 }