Typescript学び直し②
オプショナルチェーン
unefined
型、null
型のプロパティへアクセスするとエラーが起きるので、
const book = undefined;
const title = book.title; // これはTypeError: Cannot read property
const author = null;
const email = author.email; // これもTypeError
このエラーを避けるには、値がnull
, undefined
出ないことをチェックする必要がある
const book = undefined;
const title = book === null || book === undefined ? undefined : book.title;
console.log(title);
※3項演算子
条件 ? trueのときの返り値 : falseのときの返り値
cost typof a == "number" ? a : a.toString();
この3項演算子を組み合わせてチェックしていくのは大変なので、記述量を減らすのが、
オプショナルチェーン。?.
で書く。
const book = undefined;
const title = book?.title; // これだけ!!!!
ネストしても使える
const book = { author: { email: "alice@example.com" } };
const authorEmail = book?.author?.email;
※ チェーンの元が、null
or undefined
出会った場合、
undefined
が返る
const a = null;
const b = a?.name; // undefined
const book = { author: null };
console.log(book.author?.name); // undefinef
関数/メソッド呼び出し
関数/メソッド呼び出しでもオプショナルチェーンが使える。
引数カッコの前に?.
を書く。
const increment = undefined;
const result = increment?.(1); // undefined
const book = { getPrice: undefined };
console.log(book.getPrice?.()); // undefined
配列要素の参照
カギカッコの前に?.
をつける
const books = undefined;
const title = books?.[0];
オプショナルチェーンをつかった値の代入先変数の型
最後のプロパティの型 | undefined
のユニオン型になる
let book: undefined | { title: string };
const title = book?.title; // const title: string | undefined
null合体演算子と組みあわせる
undefined
が帰った時には、デフォルト値を代入したいケースは、ままある。
??
null合体演算子をつかってデフォルトを定義する。
const book = undefined;
const title = book?.title ?? "デフォルトタイトル"; // デフォルトタイトル
オブジェクトをループする
for in文(拡張 for文)
const foo = { a: 1, b: 2, c: 3 };
for (const prop in foo) {
console.log(prop, foo[prop]);
// a 1
// b 2
// c 3 の順で出力される
}
通常のオブジェクトリテラルはObject.prototype
オブジェクトが、プロトタイプになる。
const foo = { a: 1, b: 2, c: 3 };
console.log(Object.getPrototypeOf(foo) === Object.prototype); // true
なので、Object.prototype
を変更すると、全てのオブジェクトへ影響してしまう。
for-in文はプロトタイプのプロパティも含めてループする。
プロトタイプ側のプロパティが追加・削除されると意図しないループ回数になってしまうので。
そのプロパティが自身のものかをチェックする機構と合わせて使いたい。
そのときに使うのがObject.prototype.hasOwnProperty.call
const foo = { a: 1, b: 2, c: 3 };
Object.prototype.hi = "Hi!";
for (const prop in foo) {
if (Object.prototype.hasOwnProperty.call(foo, prop)) {
console.log(prop, foo[prop]);
// a 1
// b 2
// c 3 の順で出力される
}
}
それ以外
Object.entries(object)
: pythonのitems()
Object.keys(object)
: pythonのkeys()
Object.values(object)
: pythonのvaluse()
const foo = { a: 1, b: 2, c: 3 };
for (const [key, value] of Object.entries(foo)) {
console.log(key, value);
// a 1
// b 2
// c 3 の順で出力される
}
const foo = { a: 1, b: 2, c: 3 };
for (const key of Object.keys(foo)) {
console.log(key);
// a
// b
// c の順で出力される
}
const foo = { a: 1, b: 2, c: 3 };
for (const value of Object.values(foo)) {
console.log(value);
// 1
// 2
// 3 の順で出力される
}
オブジェクトをループさせるやり方その2
Object.keys()
とforEeach()
を使う
let obj: Record<string, string> = {tanuki: 'pon-poko', kitsune: 'kon-kon', neko: 'nyan-nyan'};
Object.keys(obj).forEach((key) => {
console.log(key + "は" + obj[key] + "と鳴いた!");
});
オブジェクトの値をインデックスで取得する
let obj: Record<string, string> = {tanuki: 'pon-poko', kitsune: 'kon-kon', neko: 'nyan-nyan'};
console.log(obj[Object.keys(obj)[0]])
構造部分型(structual subtyping) サブタイプの話
- 基本型: supertype -> 派生型: subtype
- 派生型には公称型(normal typing)と構造的部分型(structural subtyping)がある。
- リスコフの置換原則を満たしてれば、どちらの派生型でも使える。
公称型(Nominal Typing)
Java, C++で採用されている定義。
ある型を基本型にする派生型は、互いに置換ができない。
構造的部分型(Structural Subtyping)
Go, TypeScriptで採用されている定義。
その型の見た目(シグネチャ)が等しければ置換可能である。
こちらでは、リスコフの原理をインターフェースが満たす。とも言える。
Typescriptにおける公称型クラス
TypeScriptではクラスに1つでも非パブリックなプロパティがあると、
そのクラスだけ構造的部分型ではなく、公称型として扱う。
class UserId {
private readonly id: string;
constructor(id: string) {
this.id = id;
}
}
class GroupId {
private readonly id: string;
constructor(id: string) {
this.id = id;
}
}
const userId: UserId = new GroupId("..."); // これは公称型として扱うのでエラー
配列
配列の型注釈
type array = T[]
type array = Array<T>
配列の扱い
配列はオブジェクトなので
参照を渡す。値を変更すると影響を受ける
const a = [1, 2, 3]
const b = a
b[0] = 1000
console.log(b)
console.log(a)
また、javascriptではpythonと違い、等価比較できない。
const a = [1, 2, 3];
const b = [1, 2, 3];
a == b; // false
jsでは配列の等価を判定する組み込みの演算子やメソッドはないので、
外部パッケージを使う。
配列のコピー
配列のコピーはスプレッド構文でやる...
※ただしshallow copy
const arr = [1, 2, 3];
const backup = [...arr]; // スプレッド構文
arr.push(4); // 変更
console.log(backup); // [1, 2, 3]
スプレッド構文
- 配列の連結
const arr = [1, 2, 3];
const arr2 = [4, 5, 6];
const concated = [...arr, ...arr2];
const arr3 = [1, ...arr2, 7];
- 配列の要素へのアクセス
index外の要素にアクセスしてもエラーとはならないundefined
が返る
tsconfg.json
のnoUncheckedIndexedAccess
をONにすると、
配列から抽出した要素は、配列の要素の型 | undefinded
として扱うことが矯正される
配列の分割代入
先頭から複数個の要素を取り出して代入する
const oneToFive = [1, 2, 3, 4, 5];
const [one, two, three] = oneToFive;
console.log(one); //1
console.log(two); //2
console.log(three); //3
ただし、tsconfig.jsonのnoUncheckedIndexedAccess
を有効にすると
取り出した要素の型はundefined
型とのユニオン型になる。
const oneToFive = [1, 2, 3, 4, 5];
const [one, two, three] = oneToFive;
const num: number = one;
// 上はコンパイルエラーになる。
// oneはnumber | undefinedになり、numberには代入できないため。
// 型アサーションが必要
ネスト要素の分割代入
const twoByTwo = [
[1, 2],
[3, 4],
];
const [[one, two], [three]] = twoByTwo;
途中要素の分割代入
カンマの数で取り出す配列を指定できる
const oneToFive = [1, 2, 3, 4, 5];
const [, , , four, five] = oneToFive;
残余部分の代入(!= スプレッド構文)
JavaScriptの配列を分割代入するときに、残余パターン(..
.)を用いて、配列の残りの部分を取り出して変数に代入できる。
const oneToFive = [1, 2, 3, 4, 5];
const [one, ...rest] = oneToFive;
console.log(one); //1
console.log(rest); //[ 2, 3, 4, 5 ]
読み取り専用配列
readonly T[]
または
ReadonlyArray<T>
で定義する。
読み取り専用にすると、破壊的変更を加えるメソッドは呼べない
const nums: readonly number[] = [1, 2, 3];
// @ts-ignore <=こういう感じで書くと以下のエラーを無視できる。
nums.push(4); // 本来コンパイルエラーになるが無視する
console.log(nums)
もしくは型アサーションで別の型にする(goと型アサーションの意味が違う)
const readonlyNumbers: readonly number[] = [1, 2, 3];
const writableNumbers: number[] = readonlyNumbers as number[];
配列に非破壊的メソッドと、破壊的メソッドがある。
破壊的メソッドを使うと、同一参照のメソッドの内容が変更されてしまうので、
スプレッド構文...
を利用して配列をshallow copyして使うと良い。
※shallow copyなので配列の中の参照型の値は影響されないので注意。
配列をループする
for文
const arr: number[] = [1, 2, 3, 4];
for (let i = 0; i < arr.length; i++) {
const b: number | undefined = arr[i]
}
console.log(i) // このスコープは参照できない
for-of文
const arr: number[] = [1, 2, 3, 4];
for (const a of arr) {
console.log(a)
}
for-ofでインデックスを取得する
entries
メソッドを使うと、indexを使える
const words = ["I", "love", "TypeScript"];
for (const [index, word] of words.entries()) {
console.log(index, word);
}
Array.forEachメソッドを使う
const words = ["I", "love", "TypeScript"];
words.forEach((value, i) => {
console.log(value, i)
})
for-in文は使わない
const arr = ["a", "b", "c"];
arr.foo = "bar"; // 追加のプロパティ typescirptだとエラーだけど使わないことを覚える
for (const x in arr) {
console.log(x, arr[x]);
// 0 a
// 1 b
// 2 c
// foo bar が順に出力される
}
共変性
- union 型 から、特定の型が削除されている場合
- 特定の型が 交差型 に追加されている場合
- クラスの型が、子クラスの型に変更されている場合
- その型自身、その部分型(subtype)が代入でいる
⇢この逆が反変性
- その型自身、その部分型(subtype)が代入でいる
### 配列の共変性で発生する問題
サブクラスの配列に対して、親クラスの要素を代入できてしまうと問題が起きる
(と、かいてあるが、これは、参照を渡さないとコストがかかるケースで起きる問題)
実行するまでわからない問題。
interface Animal {
isAnimal: boolean;
}
interface Dog extends Animal {
wanwan(): string; // メソッド
}
const pochi = {
isAnimal: true,
wanwan() {
return "wanwan"; // メソッドの実装
},
};
const dogs: Dog[] = [pochi];
const animals: Animal[] = dogs;
animals[0] = { isAnimal: true }; // 同時にdogs[0]も書き換わる
const mayBePochi: Dog = dogs[0];
mayBePochi.wanwan(); //これは実行するまでエラーになることがわからない
なぜ共変なのか
nullを許容する配列をうける関数を考えると、
引数の型は (T | null)[]
である。
不変であると、T[]を渡したいのに、毎回型アサーションを噛ませないとあ足せなく、
利便性が低いため
タプル
-
定義
const: list [T, U, V]
-
アクセス
index
列挙(enum)型
- 数値の列挙型
enum Position {
Top = 1, // 1 連番の値を指定することもできる
Right, // 2
Bottom, // 3
Left, // 4
}
- 文字列の列挙型
enum Direction {
Up = "UP",
Down = "DOWN",
Left = "LEFT",
Right = "RIGHT",
}
indexアクセス
enum Direction {
Up = "UP",
Down = "DOWN",
Left = "LEFT",
Right = "RIGHT",
}
// console.log(Direction["Huga"]) // これはエラー
enum IntDirection {
UP,
DOWN
}
console.log(IntDirection["UP"]) // 0
console.log(IntDirection[0]) // UP index指定でメンバー名を取れる
console.log(IntDirection[100]) // undefined; 存在しないindexも指定できる。
ただし、IDEではインデックス指定の参照は警告を吐いてはくれる
こういう参照の仕方であればOK
console.log(IntDirection[IntDirection.DOWN]) // 0
Enumの代替
ユニオン型
type YesNo = "yes" | "no";
function toJapanese(yesno: YesNo) {
switch (yesno) {
case "yes":
return "はい";
case "no":
return "いいえ";
}
}
Symbolを使える(Symbol型の型は変数名型の型エイリアスになる)
const yes = Symbol();
const no = Symbol();
type YesNo = typeof yes | typeof no;
function toJapanese(yesno: YesNo) {
switch (yesno) {
case yes:
return "はい";
case no:
return "いいえ";
}
}
オブジェクトリテラル
- enum相当のオブジェクトを自分で作ってconst assertionをかける。
-
typeof T[keyof tyoeof T]
をつかうと、オブジェクトの値を取るユニオン型になる。
const Position = {
Top: 0,
Right: 1,
Bottom: 2,
Left: 3,
} as const;
type Position = typeof Position[keyof typeof Position];
// 上は type Position = 0 | 1 | 2 | 3 と同じ意味になります
function toJapanese(position: Position) {
switch (position) {
case Position.Top:
return "上";
case Position.Right:
return "右";
case Position.Bottom:
return "下";
case Position.Left:
return "左";
}
}
ユニオン型
ユニオン型の配列は以下の様に()
でスコープを明示
type List = (string | number)[]
判別可能なユニオン型
プロパティが異なる型のユニオンの判別
=>ディスクリミネータを使う
ユニオン型に含まれる各型で、同名のプロパティをもたせる。
そのプロパティの値で、分岐すれば型の判別ができる
ディスクリミネータに使えるのはリテラル型とnull
, undefined
type OkOrBadRequest =
| { statusCode: 200; value: string }
| { statusCode: 400; message: string };
function handleResponse(x: OkOrBadRequest) {
if (x.statusCode === 200) {
console.log(x.value);
} else {
console.log(x.message);
}
}
//こう書いてもいい
function handleResponse2(x: OkOrBadRequest) {
const {statusCode} = x
if (statusCode === 200) {
console.log(x.value);
} else {
console.log(x.message);
}
}
definite assignment assertion
- 変数宣言の変数名、プロパティ名のあとに
!
をつける - コンパイラに確実に初期化されることを伝える
let num!: number;
// ^definite assignment assertion
initNum();
console.log(num * 2); // エラーにならない
function initNum() {
num = 2;
}
class Foo {
num!: number;
// ^definite assignment assertion
}
非nullアサーション
console.log(num! * 2); // エラーにならない
等価
===
型も一致していないとだめ
==
: 型を勝手に合わせてくれる
なので基本は厳密等価を使うが、
null値に関しては、等価演算子をつかうとundefinedに関する記述を減らせるが、
明示しておくかどうか、といった感じ
console.log(null == null) // true
console.log(undefined == null) //true
console.log(null === null) // true
console.log(undefined === null) //true
オブジェクトは以下な感じ
const a = {a: "1", b: "2"}
const b = {a: "1", b: "2"}
const c = a
console.log(a == b) // false
console.log(a === b) // false
console.log(a == c) // true
console.log(a === c) // true
Recordとindex型
これは同じ
type MyRecord = Record<string, string>
type MyIndex = { [key: string]: string }
インデックス型は任意のプロパティを持つ形でもかける
type MyIndex = {
a: string,
[key: string]: string
}