なんとなくで書くTypeScriptからの脱却(基礎の基礎は書かない)
公式のTypeScriptのweb実行環境
以下にアクセスして型定義がどうなるかすぐに確認できる。
TypeScriptの特徴
TSの型がランタイムに影響を与えない。
never型
どんな値も入らない型。
let neverValue: never
const neverValue = "test" // エラー
const neverValue = 100 // エラー
const neverValue = null // エラー
const neverValue = undefined // エラー
【利用シーン】
◆関数の戻り値
戻り値がない関数に利用する。voidとの違いとしてはnever型はreturnを書くとエラーになる。
const test = ():never => {
return; // エラー
}
◆switch文(if-elseでも実現可)
次のような処理があったとします。
const arr: ["A", "B"] = ["A", "B"];
const index = Math.floor(Math.random() * 2);
const value: "A" | "B" = arr[index];
switch (value) {
case "A":
break;
case "B":
break;
default:
const neverVal:never = value;
}
default句のneverValはnever型でvalueもnever型になるのでエラーとなりません。
(case句でAかBであればbreakするためdefault句内はnever型になります。)
ここで、valueの型に"C"を足します。
const arr: ["A", "B", "C"] = ["A", "B", "C"];
const index = Math.floor(Math.random() * 2);
const value: "A" | "B" | "C" = arr[index];
switch (value) {
case "A":
break;
case "B":
break;
default:
const neverVal: never = value; // ここでエラー
}
すると、neverValがエラーになります。defalut句のvalueにマウスをホバーするとvalueはC型であることがわかりました。このようにnever型を利用するとcase句の追加忘れに気づくことができます。
型エイリアス(typeによる型定義)
◎変数名は慣習的にアッパーキャメルケース(またはパスカルケースともいう)で記述する。
◎型エイリアスは名前の通りエイリアスであり変数ではない。ので再代入はできない。
type Human = {
name: string;
}
Human = {
age: number; // 'Name' only refers to a type, but is being used as a value here.
// Name は型として参照されるだけですが、ここでは値として使われています。
}
◎必ずトップレベルで宣言する必要がある。関数内で宣言するとエラーになる。
型エイリアスとinterfaceの違いは?
実はそんなに違いはなくどちらを利用しても問題はない。のでプロジェクト内でルールの統一がされていればどちらでもよい。
ただし等価ではない。
◎型の融合方法がことなる。
型エイリアスの場合
type Name = {name: string} ;
type Age = {age: number} ;
type Human = Name & Age
interfaceの場合
interface Name {name: string}
interface Age {age: number}
interface Human extends Name, Age {}
◎同名の型宣言
型エイリアスではエラーになる。
interfaceではエラーにならないでマージされる。
interface Human {name: string}
interface Human {age: number}
const human: Human = {
name:"sato",
age:20,
}
型アサーション
asと書くことで実現できる。型推論を強制的に開発者側の型に置き換える。
これは、型の管理を開発者側で行う必要があるということ。
asを利用する際はその周辺の型定義を注意深く観察し管理しなければならないことに注意する。
アサーションのでエラーが出る原因
アサーションするときにエラーになることがあります。これはアサーションされる変数の型とアサーションで指定した型が包含関係にないときに起きます。
const a:A = {name:"taro"};
const b:B = {age:20};
a = b as A; // エラー(Property 'name' is missing in type 'B' but required in type 'A'.)
また、string型とnumber型でもエラーになります。
let str = "A"
const num = 10;
str = num as string // エラー
これはnumber型はstrign型のもつ関数などを持っていないからエラーとなります。
解決方法
両社が包含関係であればアサーションが可能です。部分集合あればアサーションできます。
// 型エイリアス
type A = {
name: string
age: number
}
type B = {
age: number
}
// bはaの部分集合なので両者は相互にアサーションすることができる。
let a:A = {name:"taro",age: 100};
let b:B = {age:20};
b = a as B // nameプロパティ
a = b as A // bが持たないnameプロパティを持つことになる
型アサーションとキャストの違い
実は2つには明確な違いがあります。
型アサーションはコンパイル時の型チェックのときのみ有効で、ランタイムの挙動には影響を与えません。
literal type widening
let変数へ値を代入すると抽象的な型になるという機能のことです。
const A = "A"; //A型になる
let B = "B" // string型になる
また、オブジェクトのプロパティに指定された場合は「literal type widening」が効く。
const A = "A"
const C = {
A, // string型
}
このようにletやobjectのプロパティは抽象的な型へ変化します。実はこの2つには同じ特徴があります。どちらもミュータブルな値です。ですので、「literal type widening」はミュータブルな値に適用されると覚えましょう。
ジェネリクス
動的な型付けを実現することができる。
引数に対応して戻り値を変える関数を作成したいときジェネリクスは活躍します。
ジェネリクスを利用しない場合
ジェネリクスを利用しないで複数の型を定義する場合、次のようにユニオン型で引数を定義します。
// ジェネリクスを利用しない
const returnValueFunc = (value:string | number) =>{
return value
}
// 下記のどちらの値もstring | numberのユニオン型です。
const personName = returnValueFunc("taro");
const personAge = returnValueFunc(10);
これで、複数の型を受け取る関数を作ることができました。しかし、問題は戻り値です。
戻り値はユニオン型になってしまいます。
また、引数が増えたときに都度引数の型定義を書き換える必要があります。ジェネリクスはこれらの問題を解決します。
ジェネリクス
ジェネリクスを利用して先ほどの関数を定義すると次のように書けます。
// ジェネリクス <T,U><第一引数の型,第二引数の型>
const returnValueFuncG = <T>(value: T) => {
return value
}
// personNameJ
const personNameG = returnValueFuncG("taro");
const personAgeG = returnValueFuncG(10);
アロー関数の頭に<T>と書くことでジェネリクスを利用することができます。"T"は受け取った引数の型に合わせて変化します。returnValueFuncG("taro");なので"T"はtaro型になるということです。戻り値は"T"より推論されるので、記載しなくてもOKです。
また、上は型推論によってpersonNameG は"taro"型になっています。constはリテラル型になるのでした。これをstring型にすることもできます。次のように"T"の型を引数と一緒に渡します。
const personNameG = returnValueFuncG<string>("taro");
const personAgeG = returnValueFuncG<number>(10);
これでpersonNameG はstring型になります。
ジェネリクスの構文について
ジェネリクス基本的な使い方は以下の通り。
const returnValueFuncG = <T>(value: T) => {
return value
}
<T>と書いているがこれは任意に決められる。慣習的には以下のアルファベットの大文字が使われる。
- T
- S
- U
また、複数定義することもできる。
const returnValueFuncG = <T, S>(name: T, age: S) => {}
配列の型定義
配列の型定義は次に3つがあります。
const arrA:string[] = [];
const arrB:Array<string> = [];
const arrC = new Array<string>();
Array<string>とありますが、これはジェネリクスです。つまり、TSの組み込みの機能として配列の定義が提供されているということです。
ジェネリクスとextends
ジェネリクスは引数を受け取って初めて型が確定します。ですので、次のようなコードはエラーとなります。
type FF = "タコス" | "たこやき" | "焼肉" |"らーめん";
const favoriteFoods:FF[] = ["たこやき","らーめん"]
const checkRamen = <T>(values: readonly T[]) => {
const food = "らーめん"
values.some((values) => values === food); // ここでエラー
}
const result = checkRamen<FF>(favoriteFoods);
console.log(result)
valuesはT型配列でfoodはstring型なのでエラーとなります。
これはasを利用してfoodをT型にできれば解決できそうです。
asは良くない方法のように思われますが今回は問題ないです。必ずvaluesの型は"T"になり、それを検査するときに利用するfoodも"T"であることが決まっているからです。checkRamen関数はそのための関数なので当たり前です。
しかし、この"決まっていること"を理解する機能はTSにはありません。ですのでasを使っても問題ないのです。
type FF = "タコス" | "たこやき" | "焼肉" |"らーめん";
const favoriteFoods:FF[] = ["たこやき","らーめん"]
const checkRamen = <T>(values: readonly T[]) => {
const food = "らーめん" as T // 次はここでエラー
values.some((values) => values === food);
}
const result = checkRamen<FF>(favoriteFoods);
console.log(result)
今度はasでエラーが起きました。asのエラーは包含関係にないときにおこります。string型とT型のどちらも部分集合ではなかったようです。これを解決するためにextendsを利用します。
type FF = "タコス" | "たこやき" | "焼肉" |"らーめん";
const favoriteFoods:FF[] = ["たこやき","らーめん"]
const checkRamen = <T extends string>(values: readonly T[]) => {
const food = "らーめん" as T
values.some((values) => values === food);
}
const result = checkRamen<FF>(favoriteFoods);
console.log(result)
これでエラーがなくなりました。<T extends string>ように書くことでT型はstring型を持つようになります。(string型がT型の部分集合となります。)。
T型は最低限string型を持っているのでアサーションすることができるようになります。
また、T extends stringとなったことで、T型でかつ最低限string型である配列しか受け付けなくなり、開発者から見れば何を渡すべきか明確になりました。
typeof による型の取得
typeofを使ってみる型の見え方はJSとTSで異なる。
let str = "TypeScript";
let num = 100;
const arr = ["TS"];
const obj = {
name:"taro",
age:20,
}
//JavaScript(コンパイル→ランタイム結果)
console.log(typeof str); // string
console.log(typeof num); // number
console.log(typeof arr); // object
console.log(typeof obj); // object
// TypeScript(コンパイル時の型チェック)
type Str = typeof str; // string
type Num = typeof num; // number
type Arr = typeof arr // string[]
type Obj = typeof obj // {name:string, age:number}
流石TS。ちゃんと厳密な型になっています。一方JSは配列とオブジェクトがobject型になってしまっています。
タプル型
固定長配列で要素の型が決まっている型。
let value:[string, number];
かなり厳密な型になっていて以下のような挙動になる。
let value:[string, number];
value = ["str", 100]; // OK
value = [100, "str"] // NG
value = ["str", 100, 100] // NG
上記のように要素の順番を入れ替えたり、多く入れたりすればエラーとなる。
as const
as const とすることでliteral type wideningを無効化できます。
※literal type wideningについては本スクラップのアサーションのコメントにて説明あり。
const name = "taro"; // taro型
const obj = {name} as const // {readonly name: "taro"}
しかも、nameプロパティがreadonlyになります。最強だ!!
as const とタプル型
配列に対してas const を利用することでタプル型を作ることができます。
const arr = ["taro", 20] as const ; // ["taro", 20]型
型の抽出
実は型定義から一部の型を抽出することができます。
type Human = {
name: string,
age: number,
}
type Name = Human["name"];
注意点としては、Human.nameのような書き方はできません。Human["name"]だけ有効です。
配列の型抽出
次のようにして型の抽出ができる。
type Names = ["taro","ziro"];
type Taro = Names[0]; // taro型
また、配列の要素指定でnumberを利用すると配列の全要素の型を抽出してユニオン型として定義できる。
type Names = ["taro","ziro"];
type UinonNames = Names[number]; // "taro" | "ziro"型
インデックスシグネチャ
オブジェクトのkey名を任意にできる型定義の機能。[key: 型]となり、key名は任意につけることができる。次のコードではkeyとなっているが本来は「lastnames」が良さそう。
type Human = {
[key:string]:{
name:string,
age:number
}
}
const human:Human = {
"katou":{
name:"tarou",
age:100
},
"tanaka":{
name:"ziro",
age:100
},
"kimura":{
name:"goro",
age:100
}
}
インデックシグネチャの問題
keyを自由に定義できるようになったので、型定義としては緩くなってしまいました。
今回は"kato" | "tanaka" | "suzuki"以外のプロパティは弾きたいです。
Mapped Typesという機能を利用すれば解決できそうです。
type Lastnames = "kato" | "tanaka" | "suzuki";
type Human = {
[key in Lastnames]:{
name: string,
age: number,
}
}
const human:Human = {
kato:{
name:"taro",
age:100,
},
tanaka:{
name:"taro",
age:100,
},
suzuki:{
name:"taro",
age:100,
},
}
Mapped Typeの構文を一般化したものは次のようになります。
[key名 in T]: U
次のような書き方もできます。
type Lastnames = "kato" | "tanaka" | "suzuki";
type Info = {
name:string,
age:number,
}
type Human = {
[key in Lastnames]:Info
}
webpack
モジュールバンドラー。モジュールバンドラーとは複数のファイルを1つのファイルにまとめる機能のこと。
ブラウザがHTMLの内容を画面に描画するとき、上からHTML読んでいきscriptタグが来たらサーバーと通信してJSの内容をとってきます。つまり、scriptタグが複数あるとリクエストが多くなり、処理に時間がかかります。ファイルを1つにまとめ通信を高速化することができます。
設定はwebpack.config.jsで管理される。
他にもいろいろできるみたい。。。調査
tsconfig.json
tsの設定ファイル
compilerOptions
TSからJSへコンパイルする際のルールや出力方法についての設定。
※出力とはコンパイルによって作られるファイルのこと
target
出力するESバージョンを指定する。
module
出力されるモジュールの方式。ES2015(import/export)であれば基本的にOK。
strict
trueとすることでtsconfig.jsonに存在する複数の型チェック設定をすべて行う。
個別に設定することもできるが、strictを使えば1発で定義できる。
strictNullCheckes
null型とundefinde型は例えばstrignなどに代入できます。
const str: string = null // OK
const str: string = undefinde // OK
// 内部的には string | null | undefined と同じ型定義
strictNullCheckesをtrueとすることで代入ができなくなる。
const str: string = null // エラー
const str: string = undefinde // エラー
// ちゃんとstring型のみ受け入れるようになる。
enum型
列挙型と呼ばれる型で「数値列挙型」と「文字列列挙型」の2つの書き方があります。
※enum型自体は非推奨の型定義らしいです。一応利用方法は確認したい。ということでenum型の機能を模倣した型定義も見ていきます。
数値列挙型
上から順番に数字が振られていくので数値列挙型と呼ばれる。
// 数値列挙型
enum Color {
Red, // 0
Blue, // 1
Yellow, // 2
}
let color:Color
color = Color.Red;
console.log(color); // => 0
console.log(Color.Red) // => 0
// 再代入
color = 1;
console.log(color); // => 1
const color2:Color = 2;
console.log(color2); // => 2
文字列列挙型
数値とは違って直接文字列を代入するとエラーになる
// 文字列列挙型
enum Color {
Red="RED",
Blue="BLUR",
Yellow="YELLOW",
}
let color:Color = Color.Yellow;
console.log(color); // => "YELLOW"
// 再代入
color = Color.Blue;
console.log(color); // => "YELLOW"
// 文字列を代入
let color2:Color = "YELLOW" // エラー
enum型の使いどころ
switch文で利用する。
型定義またはcaseのどちらか一方だけを修正するとエラーとなるため、型安全になる。
enum Color {
Red="RED",
Blue="BLUR",
Yellow="YELLOW",
Pink="PINK",
}
// エラーになる。
const checkColor = (color:Color) => {
switch(color){
case Color.Red:
return "赤"
case Color.Blue:
return "青"
case Color.Yellow:
return "黄"
}
}
enum型はなぜ使うべきではないのか?
理由は2つあります。(これ以外もあるので enum typescriptでググるとたくさん出てくる)
1つは数値列挙型が型安全ではないからです。次のようなコードはチェックを通ってしまいます。
enum Color {
"Red",
"Blue",
"Yellow",
}
const color:Color = 100;
もう一つはコンパイル後にenum型はjsファイルに残るからです。
実際に上のコードをコンパイルしたものが次にコードになります。
※下は数値列挙型になりますが、文字列列挙型でも同様です。
var Color;
(function (Color) {
Color[Color["Red"] = 0] = "Red";
Color[Color["Blue"] = 1] = "Blue";
Color[Color["Yellow"] = 2] = "Yellow";
})(Color || (Color = {}));
const color = 100;
型定義とは異なり、値を持っているためランタイムに影響を及ぼします。
enum型の代わりの型
keyof T を使う
type Color = {
Red:string,
Blue:string,
Yellow:string,
Pink:String
}
// keyof T でオブジェクトのプロパティのユニオン型を作れる
type Colors = keyof Color; // Colors型は Red | Blue | Yellow のユニオン型と同じ
const red:Colors = "Red";
console.log(red); // => "Red";
enum型で書いていたswitch文を置き換えると次のようになる。
// Pinkのcaseがないためエラーになる
const checkColor = (color:Colors) => {
switch(color){
case "Red":
return "赤"
case "Blue":
return "青"
case "Yellow":
return "黄"
}
}
【実践】複雑な型
型は利用するオブジェクトから作成するとよい。オブジェクトに変更があった場合、追従して型定義も変更されるからだ。
複雑な型定義
const colors = {
red:"RED",
blue:"BLUE",
yellow:"Blue",
} as const
type Colors = typeof colors[keyof typeof colors];
const red: Colors = "RED"; // OK
const pink: Colors = "PINK"; // エラー
type Colorsの型は現在"RED" | "BLUE" | "Blue" のユニオン型になっている。
なぜこのようになったのか1つずつ構文を見ていき解決していく。
as const
オブジェクトの型推論はliteral type wideningが働くことで抽象的な型定義がされる。as constとすることで厳密な型定義にしている。
typeof
typeofの後ろに置かれた変数の型を抽出する機能。
obj[ key名 ]
colorsオブジェクトに角かっこがついている。この構文は次のような挙動になる。
const obj = {
name:"sato",
age:20,
}
// obj[プロパティ名]でvalueを取り出せる
const age = obj["age"];
console.log(age) // 20
これはJSのオブジェクトの挙動になるが、解決したいコードはtypeof colors[]で型定義になっている。つまり、JSのオブジェクトではないが、型定義の場合も上記で示したJSオブジェクの挙動になる。また、jsでは["a"|"b"]のような書き方ができないことからも、typeof colors[ユニオン型]となっていて型定義であることがわかる。
keyof T
keyof の後ろにある、オブジェクト型のプロパティをユニオン型で取得する。
type Obj = {
food:"ramen",
age:20,
}
type UnionType = keyof Obj // food | age
const age:UnionType = "age"; // OK
const food:UnionType = "food"; // OK
const height:UnionType = "heith"; // エラー
結論
"RED" | "BLUE" | "Blue" のユニオン型にしたいなら次のような書き方ができる。
// typeof colors[プロパティ名のユニオン型];
type Colors = typeof colors["red"|"blue"|"yellow"];
このようなハードコーディングを回避するためにkeyofでユニオン型をとりだす。
ただし、keyof Tなのでkeyofの後ろは型でないといけない。
したがって次のような書く。
type tempType = keyof typeof colors;
これでtempType の型は "red"|"blue"|"yellow"となる。
Assertion Functions
asserts x is Tと戻り値に書くことで、型を確定させる。
実例は次の通り。errorがスローされなければnumber型が確定する。
function assertIsNumber(x: unknown): asserts x is number {
if (typeof x !== "number") {
throw new Error("BOOM");
}
}
// someValueはunknown型
const someValue: unknown = "foobar";
assertIsNumber(someValue);
// ここではsomeValueはnumber型
console.log(someValue.toFixed(1));
また、asserts x と書くこともできるが、この場合は引数はxはtruthyな値という意味になる。
注意点として、asと同じく型を開発者が決める仕組みになっている。また、throwするので呼び出し元でエラーハンドリングをする必要がある。
大変参考になる記事のコードを引用しました。この方のほかの記事も素晴らしすぎます。感謝永遠。。。
【参考URL】
Conditional Types
type MyCondition<T, U, X, Y> = T extends U ? X : Y;
三項演算子と同様の記法。「TがUに代入可能であればXを、そうでなければY」という型を表す。
type UrlOf<T> = T extends {url:unknown} ? "url":never;
type Result = {
message:string;
url:string;
}
type Result2 = {
message:string;
}
type ResultUrl = UrlOf<Result> // url型
type ResultUrl2 = UrlOf<Result2> // never型
【参考記事】
unknown型
any型と同様なんでも代入できる型。
異なる点としては、型アサーションされない限り利用できないので、anyより安全。
また、型がわからない値を受け取るときはunknownにしておけば、参照のみでアクセスは禁止であることがわかる。
【参考】