1. 概要
ポートフォリオにフロントエンドでReactというフレームワークを導入するが
これの言語として、AltJS
の一つであるTypeScript
を使用しようと考えている
まずは使用する前にこれの特徴と実際の使用方法について調べたことをまとめる
補足
本記事はりあクト!という非常に良質な技術同人誌を読んでみて、理解が浅い点等を外部のソースを元に試しつつ補足するような構成となっております。
リンクの書籍を一読して記事を読むとさらに理解が深まるのではないかと思っております!
雑誌に出てくる新人さんの理解力が良すぎるので都度外部のソースを調べながら記事にしました。
また、該当の書籍を読んでJavaScriptで関数型プログラミングをする時のまとめもしているので見てみてください!
2. TypeScriptとは
2.1. TypeScriptを使うメリット
JavaScript
を使用せず、TypeScript
を使用するのにはもちろん理由がある
- 静的な型チェックが行える
コンパイル時に型エラーを(全てではないが)検出することができる
-
エディタの補完機能と併用することでコードの自動補完やコンパイル前にチェックを行うことができる
-
可読性の向上(
TypeScript
に慣れている人にとっては)
型宣言を行うことにより、明示的に変数などの型をコードに残すことができる
2.2. TypeScriptを使用するデメリット
TypeScript
はJavaScript
に型情報を静的に宣言するスーパーセットという理解だけで、JavaScript
からの移行を行なったり、TypeScript
で開発を始めると、予想の開発工数を大きく超過する可能性がある
- 学習コスト
TypeScript
はJavaScript
に型情報を付加しただけの言語ではない
実際には、ライブラリを使用する場合にはtypes
と呼ばれる、ライブラリに対応する型セットをインストールし、型定義に使用する必要がある
このtypes
がライブラリのアップデート後に更新されていない場合なども考えられる
それらを使用したり、型宣言に慣れるまでの学習コストがかかる
- JavaScriptでサポートされているライブラリがすべてTypeScriptでサポートされているわけではない
JavaScript
のライブラリで有用なライブラリも、実際の開発でTypeScript
を使用する場合、サポートされていない場合(型定義パッケージがない)場合は使用することができない
上記のデメリットがあることを承知した上で、メリデメを考慮して実際の開発では採用するようにする
3. TypeScriptの流行の歴史
3.1. 昔前まではRubyやPythonのようなLL(Lightweight Language:軽量プログラミング言語)にカテゴライズされる動的型付言語が人気だった
いちいち型をかかなくていい手軽さがうけていた
3.2. LLも普及するにつれ、それなりの規模での開発が一般的となった
ソフトウェアテストの重要性が叫ばれるようになった
静的型付けがないためにメソッドの引数や返り値のテストを書くことで保証する必要性があったのも理由の一つだったと言える(TDD)
3.3. 規模が大きくなるにつれて、LLの手軽さによるメリットよりもそのコードの保守性によるデメリットのブレイクスルーを迎えてきた
その場限りのプログラムではない、今後も継続的にバージョン等を管理しながらリリースしたり、そもそも規模の大きなプログラムに対しては、テストコードだけで品質を担保するのが大変
静的型付け言語は型推論という言語処理系が文脈から型を予測して片付けできるので、コンパイルしなくてもエラーを検知できるようになっている
静的型付け言語ではそのほかにNULL安全性という特徴もある
それ以降はプログラムの規模感やリソースなどにもよるが、静的型付け言語が流行り出した
4. TypeScriptの基本
4.1. 型アノテーションについて
変数宣言時に型式を宣言できる機能のこと
value: type
というフォーマットで宣言できる
const num: number = 4;
型アノテーションによって静的に型付けされた情報はコンパイル時のチェックに用いられる
書かれたコード中に型の不整合があればコンパイルエラーにすることができる
4.2. 型推論
コンパイラがその文脈からその型を推測できる場合は、型アノテーションを省略しても自動的に補完して解釈してくれる
変数宣言時に直接リテラルを指定するような時は型アノテーションがなくても型推論で宣言できる
const sample = `user`
console.log(sample) //user
4.3. TypeScriptのプリミティブ型
TypeScript
のプリミティブ型はJavaScript
と共通した7種類
4.3.1. Boolean 型
true および false の 2 つの真偽値を扱うデータ型。型名は boolean
const samplebool: boolean = true
4.3.2. Number 型
数値を扱うためのデータ型。型名は number
const samplenum: number = 10
4.3.3. BigInt 型
number 型では表現できない大きな数値(2^53以上)を扱う型。型名は bigint
4.3.4. String 型
文字列を扱うためのデータ型。型名は string
cons samplestr: string = `user`
4.3.5. Symbol 型
「シンボル値」という固有の識別子を表現する値の型。型名は symbol
4.3.6. Null 型
何のデータも含まれない状態を明示的に表す値。型名は null
4.3.7. Undefined 型
「未定義」であることを表す値。型名は undefined
4.4. 配列の型定義
type
名の後に[]
をつけるだけで配列型を指定できる
type
名の配列を宣言でき、それ以外の型の値を挿入することはできない
const arr: number[] = [1, 2, 3]
4.5. オブジェクトの型定義
狭義のオブジェクトの型を定義する際は、プロパティのキー名と値の型を明記する形で型アノテーションをおこなう
const red: { rgb: string, opacity: number } = { rgb: 'ff0000', opacity: 1 };
だが、オブジェクトの定義を毎回インラインで記述するのは面倒
次の、インターフェースでオブジェクトの雛形を名前をつけて保存しておくことができる
4.6. インターフェース
-
<key>?: <annotation>
とすることで、省略可能になる -
readonly
修飾子をつけるとプロパティは書き換え不可になる
interface sample {
name: string,
age: number,
weight?: number,
readonly rgb: string,
}
const obj: sample = {
name: 'user',
age: 20,
rgb: '#FF0000'
}
console.log(obj.name); //user
4.7. インデックスシグネチャ(IndexSignature)
インターフェースに任意のキーのプロパティ値を定義するもの
しかし、キー値はインデックスシグネチャを指定すると、他の要素を定義したくても、キーがインデックスシグネチャと同じで、値の型が違う物は宣言できなくなるのでユースケースがわからない
interface Status {
age: number;
[attr: string]: number;
}
const sample: Status = {
age: 20,
weight: 70,
height: 170
};
4.8. リテラル型
式としてのリテラルとは関係ない(literal:文字通りの)という意味で同じ言葉が使われている
let sample: 'user' = 'user';
console.log(sample); //user
リテラル型単体では使い道がないが、演算子「|」と組み合わせることでEnumのように使用することができる(共用体型との組み合わせ)
文字列リテラル型といい、enum
と比べてもシンプルに記述できて扱いやすい
enum
はその性質上(enumの数値を変更できてしまう)扱いづらい
数値リテラルも存在するが、文字列リテラルと比べて使用するユースケースは少ない
let sample: 'user' | 'hacker' | 'admin' = 'user';
console.log(sample); //user
sample = 'hacker';
console.log(sample); //hacker
sample = 'employee'; //Type '"employee"' is not assignable to type '"user" | "hacker" | "admin"'.
4.9. タプル型
個々の要素の型とその順番や要素数に制約を設けられる特殊な型
const sample: [string, number] = ['user', 20];
ユースケースとして関数の引数などは型と順番と個数が決まっているものの典型
関数から引数の型だけ抽出するとこのタプル型で返ってくる
API関数の戻り値に複数の異なる値を設定するときにタプルを使用することがある
const sample: [number, string, boolean ] = [1, '@dmin', true];
const [id, userName, isAdmin] = sample;
console.log(id, userName, isAdmin];//1, @dmin, true
4.10. any, unknown, never
アプリケーション開発の現場では、時にはデータの型が不明なまま処理を書かなければいけないこともある
例)JSONファイルをパースしてそのままオブジェクトとして使う場合は事前に一律の型を当てはめるのが難しかったりする
4.10.1. any
なんでも定義できてしまう型
型安全ではない。戻り値でぬるぽが起きる可能性がある
非推奨の型なので、使う時はunknown型で定義された方を使用する
let sample: any = 'sample';
console.log(sample); //sample
sample = 10;
console.log(sample); //10
4.10.2. unknown
anyの型安全版で任意の型の値を代入できる点では同じ
全ての型の値はunknown
型に代入可能であるが、unknown
型の値自体は明示的な型変換や型ガードを伴う型チェックがない限りunknown
とany
を除いて代入不可
let sample: unknown = 'sample';
console.log(sample); //sample
sample = 10;
console.log(sample); //10
sample = true;
console.log(sample); //true
let boo: boolean = sample; //Type 'unknown' is not assignable to type 'boolean'.
- 型ガード
unknown
を安全に利用するための機能
typeof
やinstanceof
を使用し、値を使用する前に型を特定することで該当のスコープ内では特定の型の前提で処理を行うことができる
以下はtypeof
による型ガードの例
let sample: unknown = 'sample';
let num: number = 0;
let str: string = '';
let boo: boolean = true;
sample = 'user';
if(typeof sample === "number") {
num = sample;
} else if(typeof sample === "boolean") {
boo = sample;
} else if(typeof sample === "string") {
str = sample;
}
console.log(num); //0
console.log(str); //user
console.log(boo); //true
他種々型ガードの方法があるが、必要に際して学習する
5. 関数の型定義について
5.0. 最初の設定(tsconfig.json)
noImplicitAny
を利かせる必要がある
ルートディレクトリ にあるtsconfig.jsonコンパイラオプションのnoImplicitAnyがTrueになっていないと引数の型定義がない時にもAny
型があてがわれてコンパイルが通ってしまう
厳密に型定義をしないとTypeScriptを開発初期から使用する意味がないので必ずこれをTrueに設定しておくこと
5.1. 関数の型定義
戻り値は暗黙の型推論が聞く場合があるが、引数には必ず型を宣言する必要がある
5.1.1. 宣言文の型定義
何も返さない戻り値の型はvoidになる
下記はconsole.logで引数のarg1
の値を表示するだけの関数なので、返す値はない
よって、返り値の型はvoid
となる(宣言するときは引数の()
の後ろに: void
とつける)
function sample(arg1: string) {
console.log(arg1);
}
sample('hello'); //hello
戻り値に型がある場合の型宣言
function add(n: number, m: number): number {
return n + m;
}
console.log(add(2, 4)); // 6
5.1.2. 関数式の型定義
単純に変数にあてがう関数に宣言文と同じように引数と戻り値の型を指定するだけ
const add = function(n: number, m: number): number {
return n + m;
};
console.log(add(5, 7)); // 12
5.1.3. アロー関数の型定義
const add = (n: number, m: number): number => n + m;
const hello = (): void => console.log('Hello!');
console.log(add(8, 1)); // 9
hello(); // Hello!
5.2. 引数と戻り値をまとめて定義する方法
関数の型定義を予め行っておき、それを使用して型定義を効率的に行う方法
5.2.1. 呼び出し可能オブジェクト
関数で使用する引数の型と戻り値をインターフェースとして宣言しておくことができる
interface <something> { (<name>: <annotation>, ...): <return annotation> }
のようにして宣言する
関数にあてがうときはconst <functionName>: <interfacename> = ...
のようにして使うことで以降の引数と戻り値を宣言できる
interface someNum {
(n: number, m: number): number;
}
const add: someNum = (n, m) => n + m ;
const multiple: someNum = (n, m) => n * m;
console.log(add(1,3)); //4
console.log(multiple(5,10)); //50
5.3. ジェネリクスを用いた型宣言
5.3.1. ジェネリクスとは
引数や返り値などの型を任意に設定できる型指定方法のこと
関数に渡す引数と同じで、任意の型を<>によって引数に渡すことで、その関数の引数や戻り値の型に適用できるようになる
以下は型引数をジェネリクスによって任意の型を指定できるようにしている
function sample<T>(arg1: T): void {
console.log(arg1);
}
sample<string>('hello'); //hello
複数の値を宣言する時、型は実際に使用する際に決定するものなので、例えば以下のような書き方ではエラーになる
function sample<T>(arg1: T, arg2: T): T {
return arg1 + arg2; //arg1とarg2の型は決まっておらず、+演算子を使用できる型か不明なため
}
5.3.2. 関数宣言にインターフェースを使用する
上記のようなときはインターフェースに関数の型定義を行い、実際に関数を使用する際はインターフェースを実装する形にする
interface sample<T> { (arg1: T, arg2: T): T };
const add: sample<number> = (arg1, arg2) => arg1 + arg2;
const stringjoins: sample<string> = (arg1, arg2) => `${arg1},${arg2}`;
console.log(add(2, 3)); //5
console.log(stringjoins('sample1','sample2')); //sample1,sample2
5.3.3. アロー関数式でのジェネリック使用例
以下は関数式をアロー関数で指定した例
const samplelog = <T>(arg1: T): void => console.log(arg1);
samplelog<string>('sample'); //sample
以下は配列を返す時のジェネリックを用いた指定の例
const toArray = <T>(arg1: T, arg2: T): T[] => [arg1, arg2];
console.log(toArray(8, 3));
6. 型エリアスとインターフェース(型エイリアスを使おう)
6.1.型エイリアスの宣言方法
type
を使用して宣言する
type Unit = 'USD' | 'EUR' | 'JPY' | 'GBP' ;
type TCurrench = {
unit: Unit;
amount: number;
};
const sample1: TCurrench = {
unit: 'JPY',
amount: 120
};
for(const [key, value] of Object.entries(sample1)){
console.log(`${key}: ${value}`);
}
//"unit: JPY"
//"amount: 120"
6.2. インターフェースより型エイリアスを使用する理由
インターフェースは既存の型をその名前のままどこからでも随時拡張できる仕様があるので、思わぬバグに繋がる
以下は継承を利用したインターフェースの拡張の例
interface sample {
arg1: number;
}
interface sample1 extends sample {
arg2: number
}
const aaa: sample = {
arg1: 10
}
const bbb: sample1 = {
arg1: 100,
arg2: 200
}
昔はインターフェースにできて型エイリアスにできなことが多かった
今となっては型エイリアスの方が表現力が高いので、インターフェースを優先して使う理由がない
7. 共用体型,交差型
7.1. 共用体型
演算子|
を型の間に置くことでそれらのうちのいずれかの型が適用される複合的な型を宣言できる
上記の型エイリアスの例でUnit
に使用していた型指定はこの共用体型を使用した宣言だった
let sample: string | number = 10;
sample = 'user';
type Unit = 'USD' | 'EUR' | 'JPY' | 'GBP' ;
type TCurrench = {
unit: Unit;
amount: number;
};
const sample1: TCurrench = {
unit: 'JPY',
amount: 120
};
7.2. 交差型
演算子&
を並べていく
AかつBと複数の型を一つに結合させるもの
用途としてはもっぱらオブジェクト型の合成に使われる
同じ型でありながら必須と省略可が交差したら必須が優先される
同じプロパティで型が共通点のなものだった場合は、never
型になる
以下は共用体型と交差型を使用した例
type A = { foo: number };
type B = { bar: string };
type C = {
foo?: number;
baz: boolean;
};
type AnB = A & B; //{ foo: number, bar: string}
type AnC = A & C; //{ foo: number, baz: boolean}
type CnAorB = C & ( A | B);
const sampleAnB: AnB = {
foo: 1,
bar: 'bar'
}
const sampleAnC: AnC = {
baz: true,
foo: 2 //オプショナルと必須では必須が優先されるので宣言しないとエラー
}
const sampleCnAorBtoB: CnAorB = {
baz: false,
bar: 'user' //Bのほうだけ使用しても問題ない
}
const sampleCnAorBtoA: CnAorB = {
baz: false,
foo: 10,
bar: 'string' //AもBもどちらも使用しても問題ない
}
8. 型のNull安全性を保証する
TypeScriptを使用することのメリットの一つにNULL安全性を保障できる点があげられる
だが、TypeScriptではデフォルトの設定では
すべての型にNullとUndefinedを代入できてしまう
Null安全性が保障されず、せっかく静的型付け言語を使っているのに実行時エラーが頻発しかねない
8.1. なぜこのようになっているのか
型のNull安全性をチェックするための機能であるstrictNullchecks
という機能でNull安全性が保障される
この機能は公開当初なかったが、バージョンがV2.0になって導入された
デフォルトでNull安全な設定にしたら、過去のソースが軒並みコンパルエラーになるためデフォルトでは設定なしとした
8.2. 設定方法
コンパイラオプションstrictNullChecksを設定する
プロジェクトルートにtsconfig.json
ファイルをおいて次にような設定を行う
"strictNullChecks": true,
8.3. あえてNulを許容したい場合
8.3.1. 共用体型でNullも許容するようにする
let foo: string | null = “fuu”;
foo = null;
9. さらに高度な型表現について
9.1. typeof
通常の式では渡された値の型の名前を文字列でかえす
型のコンテキストで使うと変数から型を抽出してくれる
以下の例ではarr
でnumber[]
を定義し、そのarr
の型定義をtypeof
でnumArr
に定義している
また、型エイリアスを使用して宣言するのとインターフェースで宣言するのでは以下のように宣言方法が異なる
const arr: number[] = [1, 2, 3];
type sampleArr = typeof arr; //型エイリアスを使用したtypeofの型宣言
interface numArr { arrr: typeof arr }; //インターフェースを使用したtypeofの型宣言
const val: numArr = {arrr:[4, 5, 6]};
const val2: sampleArr = [1, 2, 3];
const val3: sampleArr = ['foo', 'bar', 'baz']; //compile error
9.2. in演算子
通常の式では指定した値がオブジェクトのキーとして存在するかどうかの真偽値を返したり
for … in文ではオブジェクトからインクリメンタルにキーを抽出するのに使われる
型コンテキストでは列挙された型の中から各要素の型の値を抜き出してマップ型物を作る
以下の例では、Fig
で共用体型と文字列リテラル型を併用した型宣言を行っている
それに対して、FigMap
の中でFig
をin
演算子で共用体型を取り出してマップ型を作成している
また、これに?
をつけることですべての要素がオプショナルな指定になるので、実際の変数に
FigMap
の型エイリアスを定義すると、実装の中ではFigMap
のキーすべてについてオプショナルな値になる
だが、この中で宣言されていない要素については型エイリアスなので拡張できない
type Fig = ‘one’ | ‘tso’ | ‘three’;
type FigMap = { [k in Fig]?: number };
const figMap: FigMap = {
one: 1,
two: 2,
// three: 3, オプショナルなので省略できる
};
figMap.four = 4; //compile error
for(const [key, value] of Object.entries(figMap)) {
console.log(`${key}: ${value}`)
}
//one: 1
//two: 2
Object.entries(figMap).map(([x, y]) => console.log(`${x}: ${y}`)); mapを使用した出力
//one: 1
//two: 2
9.3. keyof演算子
型コンテキストでは、オブジェクトの型からキーを抜き出す
- 定義していあるオブジェクトから型情報を抜き出す
const permissions = {
r: 0b100,
w: 0b010,
x: 0b001,
};
type PermsChar = keyof typeof permissions; // ‘r’ | ‘w’ | ‘x’
const sample: PermsChar = 'x';
- 型エイリアスから型情報を抜き出す
type permissions = {
r: 0b100,
w: 0b010,
x: 0b001,
}
type PermsChar = keyof permissions;
const sample: PermsChar = 'r';
学習教材
TS Playground
ブラウザでTypeScriptの挙動を確かめることができる
tsconfigの設定も変更することができるので、設定によってどのように制約がかかるかなどのチェックもブラウザで簡単にできる