Open18

Typescript学び直し②

bigbigcatatmospherebigbigcatatmosphere

オプショナルチェーン

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 ?? "デフォルトタイトル";  // デフォルトタイトル
bigbigcatatmospherebigbigcatatmosphere

オブジェクトをループする

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] + "と鳴いた!");
});
bigbigcatatmospherebigbigcatatmosphere

オブジェクトの値をインデックスで取得する

let obj: Record<string, string> = {tanuki: 'pon-poko', kitsune: 'kon-kon', neko: 'nyan-nyan'};
console.log(obj[Object.keys(obj)[0]])

bigbigcatatmospherebigbigcatatmosphere

構造部分型(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("...");   // これは公称型として扱うのでエラー
bigbigcatatmospherebigbigcatatmosphere

配列

配列の型注釈

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.jsonnoUncheckedIndexedAccessをONにすると、
    配列から抽出した要素は、配列の要素の型 | undefindedとして扱うことが矯正される
bigbigcatatmospherebigbigcatatmosphere

配列の分割代入

先頭から複数個の要素を取り出して代入する

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 ]
bigbigcatatmospherebigbigcatatmosphere

読み取り専用配列

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[];
bigbigcatatmospherebigbigcatatmosphere

配列に非破壊的メソッドと、破壊的メソッドがある。

破壊的メソッドを使うと、同一参照のメソッドの内容が変更されてしまうので、
スプレッド構文...を利用して配列をshallow copyして使うと良い。
※shallow copyなので配列の中の参照型の値は影響されないので注意。

bigbigcatatmospherebigbigcatatmosphere

配列をループする

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 が順に出力される
}
bigbigcatatmospherebigbigcatatmosphere

共変性

  • union 型 から、特定の型が削除されている場合
  • 特定の型が 交差型 に追加されている場合
  • クラスの型が、子クラスの型に変更されている場合
    • その型自身、その部分型(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[]を渡したいのに、毎回型アサーションを噛ませないとあ足せなく、
利便性が低いため

bigbigcatatmospherebigbigcatatmosphere

列挙(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
bigbigcatatmospherebigbigcatatmosphere

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 "左";
  }
}
bigbigcatatmospherebigbigcatatmosphere

ユニオン型

ユニオン型の配列は以下の様に()でスコープを明示

type List = (string | number)[]
bigbigcatatmospherebigbigcatatmosphere

判別可能なユニオン型

プロパティが異なる型のユニオンの判別

=>ディスクリミネータを使う
ユニオン型に含まれる各型で、同名のプロパティをもたせる。
そのプロパティの値で、分岐すれば型の判別ができる

ディスクリミネータに使えるのはリテラル型と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);
    }
}
bigbigcatatmospherebigbigcatatmosphere

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); // エラーにならない
bigbigcatatmospherebigbigcatatmosphere

等価

=== 型も一致していないとだめ
==: 型を勝手に合わせてくれる
なので基本は厳密等価を使うが、
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
bigbigcatatmospherebigbigcatatmosphere

Recordとindex型

これは同じ

type MyRecord = Record<string, string>

type MyIndex = { [key: string]: string }

インデックス型は任意のプロパティを持つ形でもかける

type MyIndex = { 
a: string,
[key: string]: string 
}