【学習メモ】TypeScriptを本気で理解しにかかる〜#2-1 TypeScript基礎〜
概要
今月の本気で理解しにかかる技術は TypeScript です!
バックエンドでは基本的に静的型付け言語ばかり触ってきたので、「まあそのうち気が向いたらちゃんと勉強するか!」と思ってたらそのうちがいつまで経ってもこないので今回がっつり学習していこうと思った次第です!
TypeScript 基礎編
なぜ TypeScript は生まれた?
近年、フロントエンドの技術が急速に進化する中で、SPA やモダン JavaScript の登場に伴い、フロントエンドとバックエンド(API)を分離することが一般的になってきた。このことにより大規模なアプリケーションでは、JavaScript のコードが肥大化した。しかし、JavaScript はその言語の性質上、バグを埋め込むような問題となる書き方をしていても実行するまで分からない、もしくは実行してもたまたまエラーにならないため、問題が顕在化するまで誰も気付けない...みたいなことが起こりやすかった。
その問題を解決するために TypeScript は JavaScript の superset として誕生した。superset とは既存の機能を含んだ上でそれを拡張した上位互換のようなもの。なので、JavaScript でできることは TypeScript でもできる、JavaScript の拡張版が TypeScript である。
細かい内容は以降で記述するとして、TypeScript の特徴としては端的に一点
-
JavaScript に型をつけることができる
要は JavaScript を静的型付け言語にしたものだと言えるでしょう。静的型付け言語の何が嬉しいのか?ということですが、これが正に上記で問題に挙げていた大規模開発で JavaScript がまかないきれなくなってきていた面を補完してくれるものなのです。
メリット
- JavaScript に型付けを行うことで安全に開発ができる。(不具合の元となるコードはコンパイル時にエラーを検知できる)
- とある変数やオブジェクトのプロパティ、またはオブジェクトそのものに型がついているので、どのような値が入るべきなのか分かりやすくなる。
デメリット
- 型を書く手間が増える
基本的な型
まず、ざっと見てきますが、静的型付け言語での開発経験がある人なら眺めるだけである程度理解できるのではないかと思います!特に Java や C#などの言語をされていた人であれば、一部 JavaScript 特有の型もありますが、基本的には同じ考え方かなと思います。(MicroSoft が開発しただけあって、C#との親和性は高いと感じました!)
型注釈とは
型注釈とは変数などに明示的に型を宣言することで以下のように記述します。
const test: string = "文字列";
型推論とは
右辺の値からコンパイラが自動的に判断して型を決めてくれる。明確なものはわざわざ型注釈を書かなくても型推論に任せてしまえば手間も減ります。
const test = "文字列";
boolean 型
true
or false
let isTrue: boolean = true;
// isTrue = 1; // コンパイルエラー
string 型
文字列
let name: string = "Bob";
let name2: string = "Bob";
number 型
数値型。他の言語では整数型や浮動小数点型など、数値型の中でも細かく分かれていたりしますが、TypeScript では全て Nubmer 型となります。
let count: number = 10;
let fleat: number = 1.15;
let nagative: number = -1;
その他、16 進数や 2 進数、8 進数もサポートされてます。
array 型
特定の型の配列
const fruts: string[] = ["Apple", "Banana", "Orange"]; //OK
const fruts2: string[] = ["Apple", "Banana", "Orange", 1]; //コンパイルエラー
// 2次元配列
const numberNijigen: number[][] = [
[1, 2],
[3, 4],
];
複数の型の配列を作りたい時は
const anys: (string | number | boolean)[] = [
"hoge",
1,
false,
1,
12,
true,
"test",
];
このようにすることで、string, number, bookean 型を持った配列が作れる。
tuple 型
要素番号ごとに型が決定されている配列。tuple を作る時は、型推論が効かないから、必ず型注釈する。
const person: [string, number, boolean] = ["bob", 25, false];
const person2: [string, number, boolean] = ["bob", 25, false, 1]; //これはエラー
上記の例でいうと
- 要素番号 0:string
- 要素番号 1:number
- 要素番号 2:boolean
でないといけない。
ちなみに push などで要素を追加するときはエラーにならないので、注意が必要。
any 型
なんでも OK な型。要は JavaScript の世界と同じことになる。
// 以下全部OKとなる
let anyString: any = "string";
anyString = 1;
anyString = true;
TypeScript の型安全の恩恵が受けられないので、基本使わない。
厄介なのは、any 型が絡むと他の型宣言した変数も型安全の恩恵が受けられない点です。
const num: any = 1;
let testString: string = "hoge";
testString = num; // コンパイルエラーは起きない。
なので、万が一 any 型を使用するケースに遭遇したら、もしくは any 型が使われているソースコードに遭遇したら要注意です!
void 型
関数の戻り値がない時
function sayHello(): void {
console.log("Hello");
}
sayHello の戻り値を確認すると undefined が返ってきている。
null 型
明示的に null 型を宣言することは基本ない。
let data = null; //これはany型
let data2: null = null; //これはnull型。null以外持てない
undefined 型
これも null と同じく明示的に宣言することは基本ない
let data = undefined; //これはany型
let data2: undefined = null; //これはundefined型。undefined以外持てない
never 型
呼び出し元に戻ってこない関数の戻り値に使用
例としては以下のように関数内で例外を発生させるパターン。
function error(message: string): never {
throw new Error(message);
}
void は何も返さない戻り値(undefined)
never は呼び出し元にそもそも返ってこないと理解しておこう。
object 型
オブジェクトには 2 通り書き方があります。
1.object
or {}
あらかじめ言っておくとこれらの型宣言は基本的には NG となります。
理由は object と宣言した場合、object 型であればなんでも再代入可能となってしまうためです。
let person: object = {
name: "Bob",
age: 25,
};
person = { post: "test" }; //OK
2.プロパティ含めて型注釈
let person: {
name: string;
age: number;
} = {
name: "Bob",
age: 25,
};
person = { post: "test" }; //エラー
上記のようにプロパティを含めて型注釈した場合、想定していないプロパティが渡されそうになったらエラーで弾いてくれます。
ちなみに
let person = {
name: "Bob",
age: 25,
};
型推論を使用した場合には、右辺の値から上記の 2 として推論してくれます!(チョ〜優しい)
これはネストした場合も同じです!
Type Aliases(型エイリアス)
型エイリアスなので、その名の通り、あらかじめ定義しておくことで型の省略記法が使えます!
よく使用されるのはオブジェクトの定義などでしょうか!
type Person = {
name: string;
age: number;
};
const Boby: Person = {
name: "Boby",
age: 25,
};
上記のようにtype
で宣言することにより型エイリアスを作成できます!
interface
上記の型エイリアスでオブジェクトの型をtype
で宣言していましたが、同様のことが interface でもできます。
interface Person {
name: string;
age: number;
}
const PersonObject: Person {
name: "Bob",
age: 35,
}
interface についてはまた以降で別途見ていこうと思います!
unknown 型
unknown 型は any 型をより安全に取り扱うための型
例
const numberAny: any = 10;
const numberUnknown: unknown = 10;
const sumAny = numberAny + 5; //これはOK
const sumUnknown = numberUnknown + 5; //これはコンパイルエラー
any 型は以前触れたように JavaScript の世界と同じく型についてはフル無視。
よって、上記の numberAny に数値以外が入っていた場合でもコンパイルエラーにしてくれないので、実行するまでエラーが起こるか否か判断できない。
unknown 型の場合に数値型の計算がしたいという時は、以下のように型チェックをした場合にコンパイルが通る。
if (typeof numberUnknown === "number") {
const sumUnknown = numberUnknown + 5; //これはOK
}
ということで any 型より安全に扱える。
intersection 型(交差型)
既存の型を使用して新たな型を生み出すことができる、文字通りの交差型。
以下例ですが、型同士を&
で繋ぐことで両方のプロパティを持った新たな型が生成されます。
type Pitcher = {
speed: number;
};
type Batter = {
average: number;
};
// type PitcherとBatterの両方のプロパティを持つNitoryuという型が作成できる。
type Nitoryu = Pitcher & Batter;
const OtaniShouhei: Nitoryu = {
speed: 160,
average: 0.25,
};
union 型(共用体型)
複数の型が入る変数や配列の場合に使用。
array のところで既に記述しましたが、以下のようにパイプで型を繋いでやることで複数の型を許容する。
let unionVal: number | string = 1;
unionVal = "moji"; // OK
unionVal = false; // コンパイルエラー
Literal 型
特定の値のみ許容する変数、プロパティを作成できる。
作成できるのは、string, number, boolean である。
ただ、boolean は使用するケースが見つからないので省略します。
let dayOfTheWeek2: "日" | "月" | "火" | "水" | "木" | "金" | "土" = "日"; //日〜土までの曜日のみ許容
dayOfTheWeek2 = "海"; //コンパイルエラー
ちなみに const で定義するとこの Literal 型で定義されることにより値が固定される。
enum 型
いわゆる列挙型。C#や Java などされてる方は馴染み深いかと思います!
要は特定の変数やプロパティの値が決まった値しか取らない場合に、グルーピングしておくことができる。
// 月を表す列挙型
enum Months {
January = 1,
February,
March,
April,
May,
Jun,
July,
August,
September,
October,
November,
December,
}
// コーヒーサイズを表す列挙型
enum CoffeSize {
SHORT = "SHORT",
TALL = "TALL",
GRANDE = "GRANDE",
VENTI = "VENTI",
}
列挙型はデフォルトで列挙した項目に number が割り振られます。何も指定しない場合は 0 スタートです!
上記の Months は January=1 からスタートし、December=12 まで自動で割り振られます!
これの何が嬉しいかというと
const MyCoffee = {
name: "White Moka",
size: CoffeSize.GRANDE,
};
MyCoffee.size = "hoge"; // コンパイルエラー
上記の size プロパティのように CoffeSize 型で定義されたプロパティは CoffeSize のグルーピングされた値の中からしか選択できません。
よって、適当な文字列で再代入しようとするとコンパイルエラーを起こしてくれます!
関数での型定義
関数において型で意識することは主に以下の 2 点です。
- 引数の型
- 戻り値の型
function での型宣言
今までも少し出てきましたが、function での宣言は以下のようになります。
function add(num1: number, num2: number): number {
return num1 + num2;
}
関数を保持する変数の型宣言
関数を保持する変数の型宣言は少しややこしいですが、以下のようになります。
function add(num1: number, num2: number): number {
return num1 + num2;
}
const addFn: (num1: number, num2: number) => number = add;
もし変数で型宣言する場合は、関数では型推論が働いてくれます!
const addFn: (num1: number, num2: number) => number = function (num1, num2) {
// num1 と num2 は number型
return num1 + num2;
};
const addFn2 = function addFn2(num1, num2) {
// num1 と num2 は any型(コンパイルエラー)
return num1 + num2;
};
アロー関数
アロー関数も function での宣言とあまり変わりありません。
const add = (num1: number, num2: number): number => num1 + num2;
一点、注意点としてはアロー関数の引数が一つの場合、括弧は省略できますが、TypeScript で型宣言する場合には括弧は必須となります。
const helloMessage = (message: string): string => `Hi! ${message}.`;
// これはコンパイルエラー。戻り値の型と引数の型の見分けがつかないからね...
const helloMessage = message: string: string => `Hi! ${message}.`;
callback 関数
callback 関数の場合は、上述した関数を保持する変数の型宣言を用いて以下のように記述できます。
function testCallback(num1: number, cb: (num: number) => number): number {
const num2 = cb(5);
return num1 + num2;
}
const callbackFn = (num: number): number => num * 2;
console.log(testCallback(3, callbackFn));
オプションパラメーター、デフォルトパラメーター
オプショナルなパラメーターとは、引数を省略して良いパラメータのことです。
実行時に省略した場合、undefined として評価されます。(というより、オプション引数の型は undefined とのユニオン型になる)
const add = (num1: number, num2: number, isPrint?: boolean): number => {
isPrint && console.log(num1 + num2);
return num1 + num2;
};
add(1, 2);
add(1, 2, true); //コンソールにも出力
デフォルトパラメータはオプションパラメータと同様に省略可能ですが、省略された場合のデフォルト値を決めることができます。
これは JavaScript でもお馴染みの機能ですね!
const price = (itemPrice: number, tax: number = 1.1): number => {
return itemPrice * tax;
};
console.log(price(1000, 1.15));
console.log(price(1000)); // tax = 1.1で計算
Rest パラメーター
複数個の引数を受け取れる Rest パラメータの型定義の方法は以下です。
const allSum = (...values: number[]): number => {
const addNum = values.reduce(
(accumulator: number, currentValue: number) => accumulator + currentValue
);
return addNum;
};
console.log(allSum(1, 2, 3, 4));
上記の通り、Rest パラメーターは配列になります。
オーバーロード
オーバーロードとは同名の関数で異なる引数の処理を定義すること。
他の言語を触ったことがある方であればご存知だと思いますが、TypeScript だと以下のように記述します。
// 関数シグネチャの定義
function hello(person: string): string;
function hello(persons: string[]): string;
// 関数の実態の定義
function hello(person: string | string[]): string {
if (typeof person === "string") {
return "Hello " + person;
} else {
return "Hello " + person.join(", ");
}
}
console.log(hello("Bob"));
console.log(hello(["Bob", "John", "Mike"]));
クラス
TypeScript でのクラスの書き方は以下のようになります。
class Person {
name: string;
age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
}
const bob = new Person("Bob", 20);
上記のとおり一般的によく見るプロパティとそれを初期化するコンストラクタですね。
また、それを new 演算子でインスタンスを作成しているという基本的な形です。
上記bob
は、Person 型になります。要は他の言語と同様に class は型を作る役割も担っています。
メソッド
class Person {
name: string;
age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
greeting(): string {
return `Hello ${this.name}`;
}
}
const bob = new Person("Bob", 20);
console.log(bob.greeting());
アクセス修飾子
TypeScript のアクセス修飾子には以下の 3 種類あります。
- public
- どこからでも呼び出しが可能
- 何もアクセス修飾子を指定しない場合は、デフォルトで public
- private
- クラス内のみ呼び出し可能(外部から呼び出せない)
- protected
- クラス内とその子クラスでは呼び出し可能
class Person {
name: string;
private age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
greeting(): string {
return `Hello My Name is ${this.name}, age is ${this.age}`;
}
}
const bob: Person = new Person("Bob", 20);
console.log(bob.greeting());
console.log(bob.name);
console.log(bob.age); //コンパイルエラー
上記のとおり、private の age は外部から呼び出すことはできません。また、後から値を上書きすることも不可です。
クラス内部のメソッドはもちろん使用可能です!
protected は上記で記載したとおり、基本的には private と同じですが、子クラスでの使用が可能になります。
class Person {
name: string;
private age: number;
protected from: string;
constructor(name: string, age: number, from: string) {
this.name = name;
this.age = age;
this.from = from;
}
greeting(): string {
return `Hello My Name is ${this.name}, age is ${this.age}`;
}
}
class Android extends Person {
constructor(name: string, age: number, from: string) {
super(name, age, from);
}
greeting(): string {
return `Hello My Name is ${this.name}, age is ${this.age} from is ${this.from}`; //ageでコンパイルエラー
}
}
上記のとおり、private 修飾子の age は子クラスから呼び出すことはできません。しかし、protected 修飾子の from は子クラスから呼び出すことは可能となります!
プロパティ記述の省略
コンストラクタについてもう少し詳しく見ていきます!
class Person {
name: string;
age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
}
上記のコードはプロパティとコンストラクタが存在する単純なクラスです。
以下のようにプロパティとコンストラクタをまとめて定義することも可能です。
class Person {
constructor(public name: string, public age: number) {}
}
要はコンストラクタの引数にアクセス修飾子を定義することでプロパティの宣言と初期化処理を省略できます。ただし、public の場合も省略不可です。
getter, setter
こちらも Java などではお馴染みの getter と setter です。
要は、各プロパティに対して直接アクセスさせるのではなく、get と set という関数越しにアクセスさせます。
- get:プロパティの参照
- set:プロパティの値の書き換え
class Person {
constructor(private _name: string, private _age: number) {
this._name = _name;
this._age = _age;
}
get name() {
return this._name;
}
set name(name: string) {
this._name = name;
}
set age(age: number) {
this._age = age;
}
}
上記のようにname
は参照、値の書き換えが可能。age
は値の書き換えは可能だけど、参照は不可というプロパティを作ることができました。
これらの getter、setter を用いて参照、書き換えの可否を自由に決めることができます!
注意点は getter、setter と命名が被らないようにプロパティ名は_
等を接頭辞につけて回避する必要があります。
readonly(読み取り専用)
readonly で読み取り専用のプロパティを作成できます。
class Person {
readonly name: string;
readonly age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
}
or
class Person {
constructor(public readonly name: string, public readonly age: number) {}
}
読み取り専用なので、参照はできるが、書き換え不可となります。
静的メンバ(static)
こちらも Java,C#などではお馴染みの static メンバです。
動的メンバと違い、初期化せず呼び出せるのが静的メンバです!
class Me {
static firstName: string = "Bob";
static lastName: string = "Marley";
static age: number = 20;
static greeting(): string {
return `Hello ${this.firstName} ${this.lastName}`;
}
}
console.log(Me.firstName);
console.log(Me.greeting());
上記のとおり、静的メンバのアクセスはクラス名.静的メンバ
となります。
namespace(名前空間)
これはまさに C#ですね笑
要は名前解決するためにパス階層をつけてあげるようなものです。
namespace Japanese {
export class Person {
readonly name: string;
readonly age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
}
}
namespace English {
export class Person {
firstName: string = "Bob";
lastName: string = "Marley";
age: number = 20;
constructor(firstName: string, lastName: string, age: number) {
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
}
}
}
const Jun = new Japanese.Person("Jun", 25);
上記のように同じ Person クラスでも違う namespace に定義することで競合を避けることができます。
注意点としては、外部から呼び出すためには export する必要がある点です。
ちなみに namespace の入れ子もできます。
namespace Japanese {
export namespace Tokyo {
export class Person {
name: string;
constructor(name: string) {
this.name = name;
}
}
}
export class Person {
readonly name: string;
readonly age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
}
}
継承
継承についても多言語のクラスの継承と大きく変わりありません。
class Animal {
constructor(public name: string) {}
say(): string {
return `I am ${this.name}`;
}
cry(): string {
return `u--`;
}
}
class Lion extends Animal {
speed: number;
constructor(name: string, speed: number) {
super(name);
this.speed = speed;
}
cry(): string {
return super.cry() + `GaoGao`;
}
run(): string {
return `${this.speed}km`;
}
}
const animal = new Animal("mouse");
console.log(animal.say());
console.log(animal.cry());
const lion = new Lion("Lion", 80);
console.log(lion.say());
console.log(lion.cry());
console.log(lion.run());
ざっと上で何やっているか書き出します。
- クラスの継承には
extends
でクラスを指定する - 子クラスは親クラスのメンバを全て継承する
-
super()
で親クラスのコンストラクタを呼び出せる。 - 親クラスのメソッドを呼び出す際も
suber.{メソッド}
で呼び出せる。例:super.cry();
- もちろん、子クラスのみに存在するメンバも増やすことができる。例:Lion クラスの
speed
とrunメソッド
抽象クラス、抽象メソッド
抽象クラス、抽象メソッドとは具体的な実装はしないクラス、メソッドのことです。
abstract class Animal {
abstract cry(): string;
}
class Lion extends Animal {
cry(): string {
return `GaoGao`;
}
}
上記のとおり、抽象クラスAnimal
をLion
クラスは、継承しています。
Lion クラスでは Animal で定義されている抽象メソッドcry
の具体的な処理を必ず実装する必要があります。
これの何が嬉しいのかというと、具象クラスでは、抽象クラスで定義されている抽象メソッドを必ず実装する必要があるので、実装漏れがなくなるということです。
大規模なシステムだと必要になってくる機能かなと思います。
interface
TypeScript ではクラスの多重継承はできません。その代わり interface では多重に実装することができます。
interface は基本型のセクションで少し触れたと思いますが、これから紹介する使用方法が一般的だと思われます。
interface も抽象クラスと同様に具体的な処理は何もしない、要はメソッドの一覧のようにものになります。具体的な処理はそれを実装したクラス側で必ず記述する必要があります。
interface Pitcher {
throwing(): string;
}
interface Batter {
swing(): string;
}
class Otani implements Pitcher, Batter {
constructor(public throwSpeed: number, public swingSpeed: number) {}
throwing(): string {
return `throw ${this.throwSpeed}km/h`;
}
swing(): string {
return `swing ${this.swingSpeed}km/h`;
}
}
const otanisan = new Otani(160, 150);
console.log(otanisan.throwing());
console.log(otanisan.swing());
上記のように複数の interface を Otani クラスは実装しています。
Otani クラスでは実装した全ての interface で定義されているメソッドの具体的な処理を記述する必要があります。
高度な型について
ジェネリクス型
ジェネリックとは「一般的な」や「汎用的な」といった意味合いで使用されます!
TypeScirpt では以下のように汎用的な関数を作るという意味で使用されます。
const echoType = <T>(arg: T): string => {
return typeof arg;
};
上記関数のように特定の型を指定するのではなく、T型
として抽象的な型を受け取るようにします。
このようにすることで同様の機能を有しているが、引数の型が違うものを複数用意する必要がなくなります。
ちなみに<T>
は抽象的な型を表すものとして、慣習的に用いられるものです。Type の省略だと思いますが、T 以外でも問題なく抽象的な型は定義できます。まあ特別事情がない場合は、T にしておけば良いと思います!
呼び出す側は型を指定して呼び出します!
console.log(echoType<number>(100));
console.log(echoType<string>("Hell"));
console.log(echoType<boolean>(true));
これで一つの汎用的な関数を定義することで、number,string,boolean のいずれの型でも呼び出すことが可能になりました!
関数だけでなく、クラスにもジェネリクスは適用できます!
class EchoClass<T> {
constructor(public value: T) {}
echo(): T {
return this.value;
}
}
console.log(new EchoClass<number>(100).echo());
console.log(new EchoClass<string>("Hello").echo());
console.log(new EchoClass<boolean>(true).echo());
要領は関数と同じなので、省略します!
型アサーション
ある変数の型が自明である場合、開発者が積極的に型を決定してあげた方が、より堅牢なシステムになります。
例えば、以下のlen
変数は any 型として判定されてしまい、後から文字列を代入してもコンパイルエラーになりません。
const test: any = "Test";
let len = test.length;
len = "hoge"; // OK
この場合、開発者は test に文字列が入っていることが自明であるため、length メソッドを使用していると思うので、test は string 型だということを決定してあげた方が変数 len の型も number 型として決まるので良さそうです。
以下のように型アサーションすることで変数 test の型を string として決定できます。
const test: any = "Test";
let len = (test as string).length;
len = "hoge"; // NG
もしくは以下でも同様です。
const test: any = "Test";
let len = (<string>test).length;
len = "hoge"; // NG
しかし、こちらは React の JSX 記法と干渉してしまうため、非推奨となります。
const アサーション
const アサーションは型アサーションと同様に開発者が明示的に変更できない定数として宣言することです。
例えば以下のように const で定義しても JavaScript の仕様上オブジェクトや配列の中身は変更可能です。
const profile = {
name: "hoge",
height: 170,
};
profile.name = "hoge2"; //OK
しかし、const アサーションを使うことで各プロパティを変更することができなくなります。
const profile = {
name: "hoge",
height: 170,
} as const;
profile.name = "hoge2"; //コンパイルエラー
これは内部的には profile の全プロパティが readonly で定義されたことになります。
よって、const アサーションするだけで全てのプロパティに readonly を付与する必要がなくなるので、楽ですし、抜け漏れを防ぐこともできます。
Nullable Types
コンパイラの設定にもよりますが、リテラル型にnull
を設定することはできません。
const profile: {
name: string;
age: number;
} = {
name: "hoge",
age: null, //コンパイルエラー
};
この対処法としては単純で null との Union 型としてやるだけです。
const profile: {
name: string;
age: number | null;
} = {
name: "hoge",
age: null, //OK
};
まとめ
ざっと TypeScript の基本構文を見ていきました!
次回はより実践的に React プロジェクトに TypeScript を組み込む方法を見ていきたいと思います。
Discussion