Open64

サバイバルTypeScriptを読んでみよう

asami-okinaasami-okina

unknown型とは?

・unknownは型安全なany型
・型が特定するまであらゆる操作が制限される
・使用する前に変数の型を確認する必要がある

asami-okinaasami-okina

useEffectには非同期関数は渡せない

useEffectには非同期関数を直接渡すことはできない。
渡そうとすると、コンパイルエラーになる。

OK

  useEffect(() => {
    fetchImage().then((newImage) => {
      setImageUrl(newImage.url); // 画像URLの状態を更新する
      setLoading(false); // ローディング状態を更新する
    });
  }, []);

NG

useEffect(async () => {
  const newImage = await fetchImage();
  setImageUrl(newImage.url);
  setLoading(false);
}, []);

引用:useEffectには非同期関数は渡せない

asami-okinaasami-okina

JSXには文が書けない

JSXの{}で囲った部分には、JavaScriptの式(値を返す)だけが書くことができる。
ifは文(値を返さない)であるため使うことができない。

NG

<div>{if (!loading) { <img src={imageUrl} /> }}</div>

したがって、JSXの式で条件分岐するには論理演算子や三項演算子を使う必要がある。

OK

<div>
  {loaded && <img src="..." />} ── 論理積演算子
  {loading || <img src="..." />} ── 論理和演算子
  {loading ? "読み込み中" : <img src="..." />} ── 三項演算子
</div>;

引用:JSXには文が書けない

asami-okinaasami-okina

Next.jsのSSR

Next.jsはサーバーサイドレンダリング(server-side rendering: SSR)をサポートしている。
これにより、初回読み込みの速度を向上させ、SEOやパフォーマンスに良い影響を与える。

SSRはウェブアプリケーションのレンダリングをサーバーサイドで行う技術。

通常クライアントサイドレンダリング(CSR)では、ブラウザがHTML、CSS、Javascriptファイルをダウンロードして、JavaScriptを使用してページをレンダリングするが、SSRはサーバーがHTMLを生成し、ブラウザに送信する。

Next.jsでSSRを行うには次のデータフェッチAPIの関数を使う。

  • getServerSideProps
  • getStaticProps
  • getInitialProps

これらの関数を使うことで、Next.jsで簡単にSSRを実装できる。

getServerSideProps

ページがリクエストされるたびにサーバーサイドで実行され、ページのプロパティを返す関数。
クライアントサイドでルーティングが発生した場合も、この関数がサーバーサイドで実行される。

サーバーサイドで実行されるため、データベースなどウェブに公開していないミドルウェアから直接データを取得するような処理も記述できる。

getStaticProps

静的生成するページのデータを取得するための関数で、ビルド時に実行される。
この関数を使用すると、ビルド時にページのデータを取得しておき、クライアントからのリクエスト時にはそのキャッシュからデータを返すようになる。

この関数は、リクエスト時や描画時にはデータ取得が実行されないことに注意が必要。
ユーザーログインが不要なランディングページや、内容のリアルタイムさが不要なブログなどの静的なページを構築するときに利用する。

getInitialProps

SSR時にサーバーサイドでデータ取得の処理が実行される。
クライアントサイドでルーティングが発生した場合は、クライアント側でもデータの取得が実行される。
このAPIはサーバーとクライアントの両方で実行されるため、両方の環境で動作するように実装する必要あり。

Next.js 9までのバージョンで使われていた古い方法で、Next.js 10以降では、代わりに getServerSidePropsやgetStaticPropsの使用が推奨されている。

引用:Next.jsのSSR

asami-okinaasami-okina

NextPageはページコンポーネントを表す型

NextPageはページコンポーネントを表す型。
この型を注釈しておくと、関数の実装がページコンポーネントの要件を満たしているかがチェックできる。

import { NextPage } from "next";
 
const IndexPage: NextPage = () => {
  return <div>猫画像予定地</div>;
};
export default IndexPage;

引用:NextPage

asami-okinaasami-okina

undefinedとnullの違い

大きなくくりで「値がない」ことを意味する点は共通している。

undefinedは「値が代入されていないため、値がない」
nullは「代入すべき値が存在しないため、値がない」
という微妙な違い。

もしどちらを使うべきか迷ったらundefinedを使うほうが無難。

引用:undefinedとnullの違い

asami-okinaasami-okina

constは可変オブジェクトを保護しない

constは再代入不可な変数名を宣言するだけ。
constには、可変なオブジェクトのプロパティを不変にする保護効果はない。

例えば、constでオブジェクトを宣言した場合、変数自体への再代入はできない。
しかし、オブジェクトプロパティの変更は可能

const obj = { a: 1 };
obj = { a: 2 }; // 再代入は不可
obj.a = 2; // プロパティの変更はできる

TypeScriptでオブジェクトを不変にするには、プロパティを読み取り専用にする必要がある。

引用:constは可変オブジェクトを保護しない

asami-okinaasami-okina

オブジェクト型のreadonlyプロパティ

TypeScriptでは、オブジェクトのプロパティを読み取り専用にできる。
読み取り専用にしたいプロパティには、readonly修飾子をつける

読み取り専用のプロパティに値を代入しようとすると、TypeScriptコンパイラーが代入不可の警告を出す。

let obj: {
  readonly foo: number;
};
obj = { foo: 1 };
obj.foo = 2; // Error

引用:オブジェクト型のreadonlyプロパティ

readonlyは再帰的ではない

readonlyは指定したプロパティだけが読み取り専用になる。
オブジェクトが入れ子になっている場合、その中のオブジェクトのプロパティまでをreadonlyにはしない。

例えば、fooプロパティがreadonlyで、foo.barプロパティがreadonly出ない場合、fooへの代入はコンパイルエラーになるものの、foo.barへ直接代入するのはコンパイルエラーにならない。

let obj: {
  readonly foo: {
    bar: number;
  };
};
obj = {
  foo: {
    bar: 1,
  },
};
obj.foo = { bar: 2 }; // Error
obj.foo.bar = 2; // コンパイルエラーにはならない

再帰的にプロパティを読み取り専用にしたい場合、子や孫の各プロパティにreadonlyをつけていく必要がある。

let obj: {
  readonly foo: {
    readonly bar: number;
  };
};

引用:readonlyは再帰的ではない

すべてのプロパティを一括して読み取り専用にする方法

ユーティリティ型のReadonlyを使うのも良い。
Readonlyはプロパティをすべて読み取り専用にしてくれる型。

let obj: Readonly<{
  a: number;
  b: number;
  c: number;
  d: number;
  e: number;
  f: number;
}>;

引用:すべてのプロパティを一括して読み取り専用にする方法

Readonly<T>

Readonly<T>は、オブジェクト型Tのプロパティをすべて読み取り専用にするユーティリティ型。

※ユーティリティ型
型の操作を容易にするための事前定義された型。

Readonly<T>の型引数

型引数Tには、オブジェクト型を代入する

before

type ReadonlyPerson = {
    readonly surname: string;
    readonly middleName?: string | undefined;
    readonly givenName: string;
}

after

type Person = {
  surname: string;
  middleName?: string;
  givenName: string;
};
type ReadonlyPerson = Readonly<Person>;

Readonlyの効果は再帰的ではない

Readonly<T>が読み取り専用にするのは、オブジェクト型T直下のプロパティのみ。

引用:Readonlyの効果は再帰的ではない

asami-okinaasami-okina

クラスのreadonly修飾子

TypeScriptでは、フィールドにreadonly修飾子をつけると、そのフィールドを読み取り専用にできる。

読み取り専用フィールドは、コンストラクタかフィールド初期化子でのみ値を代入できる。

class Octopus {
  readonly name: string;
  readonly legs = 8; // フィールド初期化子での代入はOK
 
  constructor() {
    this.name = "たこちゃん"; // コンストラクターでの代入はOK
  }
}

読み取り専用フィールドは、再代入しようとするとコンパイルエラーになる。

const octopus = new Octopus();
octopus.legs = 16; // Error

メソッド内の処理であっても、読み取り専用フィールドへの再代入は許されない。

class Octopus {
  readonly name = "たこちゃん";
 
  setName(newName: string): void {
    this.name = newName; // Error
  }
}

引用:クラスのreadonly修飾子

asami-okinaasami-okina

読み取り専用の配列(readonly array)

TypeScriptでは配列を読み取り専用として型注釈できる。
型注釈の方法は2通りある。

1: readonlyキーワードを使う
2: ReadonlyArray<T>を使う

readonly T[]

配列の型注釈の前にreaedonlyキーワードを添えると、読み取り専用の配列型にできる。
たとえば、readonly number[]と書くと、その変数の型はnumberの読み取り専用配列型になる。

const nums: readonly number[] = [1,2,3];

ReadonlyArray<T>

ReadonlyArray<T>のような書き方でも読み取り専用の配列型になる。
例えば、要素がnumber型の配列を読み取り専用にしたい場合、ReadonlyArray<number>と書く。

const nums: ReadonlyArray<number> = [1,2,3];

読み取り専用配列の特徴

pushしようとするとコンパイルエラーになるが、警告されるだけで配列オブジェクトからpushメソッドを削除しているわけではない。

そのため、コンパイルエラーを無視して実行してみると、読み取り専用型でも配列を書き換えることができる。

読み取り専用配列を配列に代入する

TypeScriptの読み取り専用配列を普通の配列に代入することはできない。
代入しようとするとコンパイルエラーになる。

const readonlyNumbers: readonly number[] = [1,2,3];
const writableNumbers: number[] = readonlyNumbers; // Error

これは、普通の配列はpushやpopなどのメソッドが必要なのに、読み取り専用配列にはそれが存在しないことになっているから。

どうしても読み取り専用配列を普通の配列に代入したい場合は、型アサーションを使う方法がある。

const readonlyNumbers: readonly number[] = [1,2,3];
const writableNumbers: number[] = readonlyNumbers as number[];

逆のパターンとして、普通の配列を読み取り専用配列に代入することは可能。

引用:読み取り専用の配列(readonly array)

asami-okinaasami-okina

型アサーション「as」

TypeScriptには、型推論を上書きする機能がある。
その機能を型アサーションという。

型アサーションはコンパイラに「私を信じて!私のほうが型に詳しいから」と伝えるようなもの。

型アサーションの書き方

2つある。
1つ目はas構文

const value: string | number = "this is a string";
const strlength: number = (value as string).length;

2つ目はアングルブラケット構文

const value: string | number = "this is a string";
const strLength: number = (<string>value).length;

どちらを用いるかは好みだが、アングルブラケット構文はJSXと見分けがつかないことがあるため、as構文が用いられることのほうが多いようだ。

コンパイルエラーになる型アサーション

number型をstring型にする型アサーションはコンパイルエラーになる。

const num = 123;
const str: string = num as string; // Error

このように型アサーションはコンパイラーの型推論を上書きできるとは言っても、無茶な型の変換はできないようになっている。

それでも自分の書いた型アサーションが正しい場合は、unknown型を経由することでエラーを出さないようにできる。

const num = 123;
cont str: string = num as unknown as string; // OK

型アサーションは値の型変換はせず、あくまでコンパイル時にコンパイラーに型を伝えるだけ。

引用:型アサーション「as」

asami-okinaasami-okina

プリミティブ型の種類

次の7つが存在する。

  1. 論理型(boolean): trueまたはfalseの真偽値
  2. 数値型(number): 0や0.1のような数値
  3. 文字列型(string): "hello world"のような文字列
  4. undefined型: 値が未定義であることを表す
  5. null型: 値がないことを表す
  6. symbol型: 一意で不変の値
  7. bigint型: 9007199254740992nのようなnumber型では扱えない大きな整数

上記のプリミティブ型以外は、JavaScriptにおいてはすべてオブジェクトと考えて問題ない。
配列や正規表現オブジェクトなどもすべてオブジェクト。

引用:プリミティブ型の種類

asami-okinaasami-okina

論理型

JavaScriptの論理型(boolean type)は、trueとfalseの論理値からなる型。
TypeScriptの論理型の型注釈はbooleanを使う。

const isOk: boolean = true;

TypeScriptには大文字で始まるBoolean型があるが、これはbooleanとは別の型。

boolean型

プリミティブなデータ型で、trueかfalseのどちらかを表す。

let isDone: boolean = false;

Boolean型

Booleanオブジェクトに対するラッパークラスとしての役割を持っている。
これは、プリミティブ型のboolean値をラップするために使用され、オブジェクトとして扱えるようにする。

let isDone: Boolean = new Boolean(false);
console.log(isDone); // [Boolean: false]

let otherIsDone: boolean = new Boolean(false);
console.log(otherIsDone); // [Boolean: false]

Boolean型は、ただのboolean型とは異なり、値を比較する場合には注意が必要。
Boolean型の比較は、オブジェクトの比較として処理されるため、常に完全一致でなければならない。

console.log(new Boolean(false) == false); // true
console.log(new Boolean(false) === false); // false
console.log(new Boolean(false) == new Boolean(false)); // false
console.log(new Boolean(false) === new Boolean(false)); // false

引用:論理型

asami-okinaasami-okina

数値型

JavaScriptの数値型は、1や-1などの整数と0.1などの小数点を含めた数値の型

数値リテラル

Javascriptの数値リテラルは次のように数値を見たままに書く。

123 // 整数
-123 // 整数(負の数)
20.315 // 小数

小数は小数点で始める書き方もできる。
整数も小数点で終わる書き方ができる。

0.1 === .1
5.0 === 5.

数値の区切り文字

JavaScriptの数値リテラルは可読性のためにアンダースコアで区切って書くことができる。
何桁ごとに区切るかは自由。

100_000_000 // 1億

ただし、_を先頭や末尾、小数点の前後、連続で2個以上置くことはできない。

NG

_100
100_
100_.0
100._0
1__00

数値リテラルのプロパティ

JavaScriptの数値リテラルのプロパティを直接参照する場合、小数点のドットとプロパティアクセッサーのドットが区別できないため、構文エラーになる。

NG

5.toString(); // この書き方は構文エラー

これを回避するには、ドットを2つ続けるか、数値をカッコで囲む必要がある。

OK

5..to_string();
(5).to_string();

数値型の型注釈

TypeScriptで数値型の型注釈はnumberを用いる。

const count: number = 123;

よく似た名前の型としてNumber型がありますが、これとnumberは別物なので注意してね。

特殊な数値

JavaScriptの数値型には、NaNとInfinityという特殊な値がある。

NaN

NaNは非数を表す変数。
JavaScriptでは、処理の結果数値にならない場合にNaNを返すことがある

たとえば、文字列を数値に変換するparseInt関数は数値化できない入力に対し、NaNを返す。
値がNaNであるかのチェックはNumber.isNanを用いる。

NaNは特殊で、統合比較では常にfalseとなるから注意。

console.log(NaN == NaN); // false

Infinity

無限大を表す変数。
例えば、1を0で割るとInfinityになる。

引用:数値型

asami-okinaasami-okina

小数計算の誤差

JavaScriptの小数の計算には誤差が生じる場合があるので要注意。
例えば、0.1+0.2の結果は0.3になってほしいところだが、計算結果は0.30000000000000004になる。

0.1 + 0.2 === 0.3; //=> false

これはJavaScriptのバグではなく、number型はIEEE 754という規格に準拠しており、その制約によって生じる現象。

10進数の0.2は有限小数だが、それを2進数で表すと0.0011....のような循環小数になる。
循環小数は小数点以下が無限に続くが、IEEE 754が扱う小数点以下は有限であるため、循環小数は桁の途中で切り捨てられる。

ちなみに、2進数で有限小数になる0.5や0.25などの数値だけを扱う計算は誤差なく計算できる。

0.5 + 0.25 === 0.75; //=> true

小数計算の誤差を解決するために、一度整数に桁上げして計算し、元の桁を下げる方法が考えられる。

整数の計算は誤差が生じないという特性に期待した方法。

例えば、110円の消費税込み価格を求める計算。
110に1.1を掛け算すると、誤差が生じて121円ぴったりにはならない。

110 * 1.1; //=> 121.00000000000001

そこで、110と桁上げした税率11を掛け算してから、10で割ってみる。
すると、うまく計算できる。

(110 * 11) / 10 === 121; //=> true

この方法を使う場合は、桁を戻した数値は小数になることがあり、その値には小数計算誤差問題が残り続けることに注意。

const price1 = (101 * 11) / 10; // 111.1
const price2 = (103 * 11) / 10; // 113.3
price1 + price2; // 224.39999999999998

小数計算の誤差問題を包括的に解決したい場合は、decimal.jsのような計算誤差がないパッケージを使おう。

引用:小数計算の誤差

asami-okinaasami-okina

文字列型

JavaScriptでは、ダブルクォートでもシングルクォートでもまったく同じ文字列型になる。
また、バッククォートを使っても文字列型になる。

文字列中に同じ引用符が含まれている場合はバックスラッシュでエスケープしなければならない。

'He said "madam, I\'m Adam."'
"He said \"madam, I'm Adam.\""

テンプレートリテラル

JavaScriptでバッククォーとで囲んだ文字列はテンプレートリテラルと言う。
テンプレートリテラルは、改行と式の挿入ができる。

console.log(`実際に改行を
してみる`);
// 実際に改行を
// してみる

式の挿入は${式}のように書く。

const count = 10;
console.log(`現在、${count}名が見ています。`);
// 現在、10名が見ています。

式の部分は変数だけでなく、計算式や関数を使った式も書ける。

`税込${Math.floor(100 * 1.1)}`

文字列の型注釈

TypeScriptの文字列型の型注釈はstringを用いる。

const message: string = "Hello";

名前がよく似た型にString型がありますが、stringとは異なるので注意してね。

文字列の結合

JavaScriptの文字列結合は文字列結合演算子(+)を使う。

"hello" + "world"

引用:文字列型

asami-okinaasami-okina

null型

JavaScriptのnullは値がないことを示す値。

nullリテラル

const x = null;

nullの型注釈

TypeScriptでnull型を型注釈するにはnullを用いる。

const x: null = null;

typeof演算子の注意点

JavaScriptには値の型を調べるtypeof演算子がある。
nullに対してtypeofを用いると"object"が返るので注意が必要。

console.log(typeof null);
// "object"

引用:null型

asami-okinaasami-okina

undefined型

JavaScriptのundefined型は未定義を表すプリミティブな値。
変数に値がセットされていないとき、戻り値がない関数、オブジェクトに存在しないプロパティにアクセスした時、配列に存在しないインデックスでアクセスした時などに現れる。

let name;
console.log(name);
// undefined
 
function func() {}
console.log(func());
// undefined
 
const obj = {};
console.log(obj.name);
// undefined
 
const arr = [];
console.log(arr[1]);
// undefined

undefinedリテラル

JavaScriptでは同じプリミティブ型でも、論理型や数値型がリテラルであるのに対し、undefinedにはリテラルがない。

実はundefinedは変数

undefinedの型注釈

TypeScriptでundefined型の型注釈を行うには、undefinedを用いる。

const x: undefined = undefined;

戻り値のない関数はundefinedになるが、TypeScriptで戻り値無しを型注釈で表現する場合、undefinedではなくvoidを用いる

引用:undefined型

asami-okinaasami-okina

undefinedとnullの違い

言語使用上の違い

nullは自然発生しない

undefinedは言語使用上、プログラマーが明示的に使わなくても、自然に発生してくるもの。
例えば、変数を宣言したときに初期値がなければJavaScriptはその変数にundefinedを代入する。

一方、nullはプログラマーが意図的に使わない限り発生しない

undefinedは変数

undefinedもnullもプリミティブ型の値という点は共通していますが、undefinedは変数でありnullはリテラル。

nullはリテラルなのでnullという名前の変数を作ることはできない。

typeof演算子

typeof undefined;
// "undefined"
typeof null;
// "object"

JSON

オブジェクトプロパティの値にundefinedを用いた時、そのオブジェクトをJSON.stringifyでJSON化したときにオブジェクトプロパティは削除される。

一方、プロパティの値がnullの時はJSON化したときに値が保持される。

console.log(JSON.stringify({ foo: undefined }));
// {}
console.log(JSON.stringify({ foo: null }));
// {"foo": null}

引用:undefinedとnullの違い

asami-okinaasami-okina

シンボル型

JavaScriptのシンボル型は、プリミティブ型の一種で、その値が一意になる値。
等価比較ではシンボルはシンボル名が同じであっても、初期化した場所が違うとfalseになる。

const s1 = Symbol("foo");
const s2 = Symbol("foo");
console.log(s1 === s1); // true
console.log(s1 === s2); // false

シンボルの型注釈

TypeScriptでシンボルの型注釈はsymbolを用いる。

const s: symbol = Symbol();

シンボルの用途

JavaScriptにシンボルが導入された動機は、JavaScriptの組み込みAPIの下位互換性を壊さずに新たなAPIを追加することだった。

要するに、JavaScript本体をアップデートしやすくするために導入されたものであり、アプリケーションを開発する場合に限ってはシンボルを駆使してコードを書く機会は多くない。

引用:シンボル型

asami-okinaasami-okina

bigint型

JavaScriptのbigint型は、数値型よりも大きな整数を扱えるプリミティブ型。

bigint型リテラル

整数値の末尾にnをつけてかく

const x = 100n;

bigintリテラルをTypeScriptで用いるには、コンパイラーオプションのtargetをes2020以上にする必要がある。

bigint型の型注釈

TypeScriptでbigint型を型注釈するには、biginhを用いる。

const x : bigint = 100n;

BigInt関数

bigint型はBigInt関数を使って作ることができる。
BigInt関数は第一引数に数値もしくは文字列を渡す。

const x = BigInt(100);
const y = BigInt("9007199254740991");

bigint型を数値型と計算する

bitgint型と数値型はそのままでは一緒に演算することはできない。
どちらかの型に合わせる必要あり。

数値型が小数部を持っていない限り、より表現幅の広いbigint型に合わせる方が無難。

const i = 2n + BigInt(3);
console.log(i); // 5n

引用:bigint型

asami-okinaasami-okina

ボックス化

プリミティブは一般的にフィールドやメソッドを持たない。
プリミティブをオブジェクトのように扱うには、プリミティブをオブジェクトに変換する必要がある。

プリミティブからオブジェクトへの変換をボックス化と言う。

// プリミティブ型
const str = "abc";
// ラッパーオブジェクトに入れる
const strObject = new String(str);
// オブジェクトのように扱う
strObject.length; // フィールドの参照
strObject.toUpperCase(); // メソッド呼び出し

上記の例はJavaScriptでボックス化のイメージを書いたもの。
実際のコードではプリミティブ型をStringのようなラッパーオブジェクトにわざわざ入れる必要はない。

JavaScriptには自動ボックス化という仕組みがあるから。

自動ボックス化

JavaScriptでは、プリミティブ型の値でもフィールドを参照できたり、メソッドが呼び出せる。

const str = "abc";
// オブジェクトのように扱う
str.length; // フィールドの参照
str.toUpperCase(); // メソッド呼び出し

プリミティブ型の値はオブジェクトではないため、このような操作ができるのは変。
このようなことができるのは、JavaScriptが内部的にプリミティブ型の値をオブジェクトに変換しているから。

この暗黙の変換を自動ボックス化と呼ぶ。

ラッパーオブジェクト

JavaScriptの自動ボックス化で変換先となるオブジェクトをラッパーオブジェクトと呼ぶ。
プリミティブ型とラッパーオブジェクトの対応は以下の通り。

【プリミティブ型:ラッパーオブジェクト】
boolean : Boolean
number : Number
string : String
symbol : Symbol
bigint : BigInt

MDNの読み方

numberにはメソッドもフィールドもない。
メソッドなどがあるように見えるのは、自動ボックス化でnumberがNumberオブジェクトに変換されるから。

Number.prototypeが表す意味は「Numberオブジェクトのインスタンスに生えている」ということも理解できる。

ラッパーオブジェクトとTypeScriptの型

TypeScriptでは、ラッパーオブジェクトの型も定義されている。
次のように、ラッパーオブジェクトの型を使って、型注釈を書くこともできる。

ラッパーオブジェクト型の変数にプリミティブ型の値を代入することも可能。

const bool: Boolean = false;
const num: Number = 0;
const str: String = "";
const sym: Symbol = Symbol();
const big: BigInt = 10n;

しかし、ラッパーオブジェクトはプリミティブ型に代入できない。

NG

const n1: Number = 0;
const n2: number = n1; // Error

ラッパーオブジェクト型は演算子が使えない。

NG

const num: Number = 1;
num * 2; // Error

ラッパーオブジェクト型は、そのインターフェースを満たしたオブジェクトであれば、プリミティブ型の値以外も代入できる。

const boolLike = {
  valueOf(): boolean {
    return true;
  },
};
const bool: Boolean = boolLike;

プリミティブ型の代わりにラッパーオブジェクト型を型注釈に使う利点はない。
型注釈にはプリミティブ型を使おう。

// ❌間違い
const num1: Number = 0;
// ✅正しい
const num2: number = 0;

引用:ボックス化

asami-okinaasami-okina

リテラル型

TypeScriptではプリミティブ型の特定の値だけを代入可能にする型を表現できる。
そのような型をリテラル型と呼ぶ。

例えば、次の例は数値が代入可能な型注釈。
数値であれば1でも100でも何でも代入できる。

let x: number;
x = 1;

リテラル型を用いると、1だけが代入可能な型が作れる・

let x: 1;
x = 1;
x = 100; // Error

###リテラル型として表現できるもの
リテラル型として表現できるプリミティブ型は次の通り。

  • 論理値型のtrueとfalse
  • 数値型の値
  • 文字列型の文字列
const isTrue: true = true;
const num: 123 = 123;
const str: "foo" = "foo";

リテラル型の用途

一般的にリテラル型はマジックナンバーやステートの表現に用いられる。
その際、ユニオン型と組み合わせることが多い。

let num: 1 | 2 | 3 = 1;

引用:リテラル型

asami-okinaasami-okina

any型

TypeScriptのany型は、どんな型でも代入を許す型。
プリミティブ型であれオブジェクトであれ何を代入してもエラーにならない。

let value: any;
value = 1; // OK
value = "string"; // OK
value = { name : "オブジェクト"}; // OK

また、any型の変数については、これ以上コンパイラーが型チェックを行わない。
実行してエラーになるようなコードでもコンパイラーは指摘しない。

暗黙のany

型を省略してコンテキストから型が推論できない時、TypeScriptは暗黙的に型をany型として扱う。

例えば、引数の型注釈を省略した場合。

TypeScriptでは暗黙のanyを規制するオプションとしてnoImplicit(暗黙)Anyが用意されている。
tsconfig.jsonにてnoImplicitAny: true を設定することで、TypeScriptが型をany型と推測した場合にエラーが発生するようになる。

引用:any型

asami-okinaasami-okina

プリミティブ以外はすべてオブジェクト

JavaScriptでは、プリミティブ型以外のものはすべてオブジェクト型。
オブジェクト型にはクラスから作ったインスタンスだけでなく、クラスそのものや配列、正規表現もある。

プリミティブ型はあたいが同じであれば同一のものと判定できるが、オブジェクト型はプロパティの値は同じであってもインスタンスが異なると同一のものとは判定されない。

const value1 = 123;
const value2 = 123;
console.log(value1 == value2);
// true
 
const object1 = { value: 123 };
const object2 = { value: 123 };
console.log(object1 == object2);
// false

引用:プリミティブ以外はすべてオブジェクト

asami-okinaasami-okina

オブジェクトリテラル

JavaScriptの特徴は**オブジェクトリテラル{}**という記法を用いて、簡単にオブジェクトを生成できる点。

// 空っぽのオブジェクトを生成
const object = {};

// プロパティを指定しながらオブジェクトを生成
const person = { name: "Bob", age: 25};

Objectをnewすることでオブジェクトを作ることができる。
しかし、オブジェクトリテラルを使った方が端的で読みやすいコードになる。

const person = ner Object();
person.name = "Bob";
person.age = 25;

引用:オブジェクトリテラル

asami-okinaasami-okina

オブジェクトのプロパティ

JavaScriptのオブジェクトは、プロパティの集合体。
プロパティはキーと値の対。

プロパティの値には、1や"string"のようなプリミティブ型や関数、そしてオブジェクトも入れることができる。

const product = {
  name: "ミネラルウォーター",
  price: 100,
  getTaxIncludedPrice: function () {
    return Math.floor(this.price * 1.1);
  },
  shomikigen: new Date("2022-01-20"),
};

上のgetTaxIncludedPriceには関数が代入されているが、この関数は**「メソッド」**と呼ばれる。

メソッドとは、オブジェクトに関連づいた関数のこと。
メソッドを定義するには、キーと関数の値に分けて書く方法だけでなく、メソッド定義のための短い構文を使うこともできる。

const object = {
  // キーと値に分けて書いたメソッド定義
  printHello1: function () {
    console.log("Hello");
  },
  // 短い構文を用いたメソッド定義
  printHello2() {
    console.log("Hello");
  },
};

メソッドにnullを代入すると、フィールドを変えてしまうこともできる。

const calculator = {
  sum(a, b) {
    return a + b;
  },
};
 
calculator.sum(1, 1);
// 2
calculator.sum = null;
calculator.sum(1, 1); // ここではもうメソッドではないので、呼び出すとエラーになる

引用:オブジェクトのプロパティ

asami-okinaasami-okina

オブジェクトの型注釈

TypeScriptでオブジェクトの型注釈は、JavaScriptオブジェクトリテラルのような書き方でオブジェクトプロパティをキーと値の型のペアを描きます。

let box: { width: number; height: number };
//       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^型注釈
box = { width: 1080, height: 720 };

プロパティの区切り文字には、オブジェクトリテラルのようにカンマ,も使えますが、セミコロン;を用いるほうを推奨される。

理由は、コード整形ツールPrettierがオブジェクト型注釈を直すとき、カンマをセミコロンに置き換えるため。

インラインの型注釈の代わりに、型エイリアスを使った型注釈の書き方もできる。

// 型エイリアス
type Box = { width: number; height: number };
let box: Box = { width: 1080, height: 720 };
//       ^^^型注釈

メソッドの型注釈

オブジェクトの型注釈には、メソッドの型注釈を書くこともできる。
書き方はJavaScriptのメソッド構文に引数と戻り値の型注釈を加えたようなものになる。

let calculator: {
  sum(x: number, y: number): number;
};
 
calculator = {
  sum(x, y) {
    return x + y;
  },
};

メソッドの型注釈は関数構文の書き方もできる。

let calculator: {
  sum: (x: number, y: number) => number;
};

Record<Keys, Type>

連想配列のようなキーバリューのオブジェクト型を定義する場合、ユーティリティ(便利)型のRecordを使う方法もある。

let foo: Record<string, number>; // キーバリューからオブジェクトを作る
foo = { a: 1, b: 2 };

object型

オブジェクトの型注釈にはobject型を用いることもできる。

let box: object;
box = { width: 1080, height: 720 };

object型の使用はおすすめされない。
何のプロパティがあるかの情報がないため。

そのため、box.widthを参照するとコンパイルエラーになる。

引用:オブジェクトの型注釈

asami-okinaasami-okina

readonlyとconstの違い

JavaScriptでは、constで宣言した変数は代入不可になる。
TypeScriptではオブジェクト型のプロパティにreadonly修飾子をつけると、そのプロパティが代入不可になる

これら2つの機能は「代入不可」という点では似ているが違いは何?

constは変数への代入を禁止にするもの

constは変数への代入を禁止するもの。
例えば、constで宣言されたxに値を代入しようとすると、TypeScriptではコンパイルエラーになり、JavaScriptでは実行時エラーになる。

const x = 1;
x = 2; // Error

constの代入禁止が効くのは変数そのものへの代入だけ。
変数がオブジェクトだった場合、プロパティへの代入は禁止される。

const x = { y: 1};
x = { y: 2}; // 変数そのものの代入は不可 NG
x.y = 2; // プロパティへの代入は許可

### readonlyはプロパティへの代入を禁止にするもの
TypeScriptのreadonlyはプロパティへの代入を禁止にするもの。
readonlyがついたプロパティxに値を代入しようとすると、コンパイルエラーになる。

```typescript
let obj: { readonly x: number } = { x: 1 };
obj.x = 2; // Error

一方、変数自体の代入は許可される。

let obj: { readonly x: number } = { x: 1 };
obj = { x: 2 }; // 許可される

constとreadonlyの違い

constは変数自体を代入不可にするもの。
変数がオブジェクトの場合、プロパティへの代入は許可される。

一方、readonlyはプロパティを代入不可にするもの。
変数自体を置き換えるような代入は許可される。

以上の違いがあるため、constとreadonlyを組み合わせると、変数自体とオブジェクトのプロパティの両方を変更不能なオブジェクトを作ることができる。

const obj: { readonly x: number } = { x : 1 };
obj = { x: 2 }; // Error
obj.x = 2; // Error

引用:readonlyとconstの違い

asami-okinaasami-okina

オブジェクト型のオプションプロパティ

TypeScriptでオブジェクトプロパティのオプショナルを型付けするには、プロパティ名の後ろに?を書く

let size: { width?: number };

オプションプロパティを持ったオブジェクト型には、そのオプションプロパティを持たないオブジェクトを代入できる。

size = {}; // OK

また、オプションプロパティの値がundefinedのオブジェクトも代入できる。

size = { width: undefined }; // OK

しかし、オプションプロパティの値がnullの場合は代入できない。

size = { width: null }; // Error

引用:オブジェクト型のオプションプロパティ

asami-okinaasami-okina

余剰プロパティチェック

TypeScriptのオブジェクト型には余剰プロパティチェックという追加のチェックが働く場合がある。

余剰プロパティチェックとは、オブジェクト型に存在しないプロパティを持つオブジェクトの代入を禁止する検査。

たとえば、{ x: number }はプロパティxが必須なオブジェクト型。
この型に{ x: 1, y: 2 }のような値を代入しようとすると、この代入は許可されない。

let onlyX: { x: number };
onlyX = { x: 1 }; // OK
onlyX = { x: 1, y: 2 }; // コンパイルエラー

こうした余計なプロパティを許さないTypeScriptのチェックが余剰プロパティチェックである。

余剰プロパティチェックはオブジェクトリテラルだけを検査する

余剰プロパティチェックはオブジェクトの余計なプロパティを禁止するため、コードが型に厳密になるよう手助けをする。

しかし、余剰プロパティチェックが効くのはオブジェクトリテラルの代入に対してのみ。
変数代入にはこのチェックは働かない。

const xy: { x: number; y: number } = { x: 1, y: 2 };
let onlyX: { x: number };
onlyX = xy; // OK

変数代入にも余剰プロパティチェックが働いたほうが良さそうと思われるかもしれない。
しかし、そうなっていないのは、TypeScriptが型の安全性よりも利便性を優先しているため。

引用:余剰プロパティチェック

asami-okinaasami-okina

インデックス型

TypeScriptでオブジェクトのフィールド名をあえて指定せず、プロパティのみを指定したい場合がある。

その時に使えるのがインデックス型
例えばプロパティがすべてnumber型であるオブジェクトは次のように注釈する。

let obj: {
  [K: string]: number;
};

フィールド名の表現部分が[K: string]。
このKの部分は型変数。

任意の方変数名にできる。

Kやkeyにするのが一般的。
stringの部分はフィールド名の方を指定する。

インデックス型のフィールド名はstring、number、symbolのみが指定できる。

インデックス型のオブジェクトであれば、フィールド名が定義されていないプロパティも代入できる

たとえば、インデックス型{ [K: string]: number }には、値がnumber型であればaやbなど定義されていないフィールドに代入できる。

let obj: {
  [K: string]: number;
};
 
obj = { a: 1, b: 2 }; // OK
obj.c = 4; // OK
obj["d"] = 5; // OK

コンパイラーオプションのnoUncheckedIndexedAccessを有効にした場合、インデックス型ではプロパティの型は自動的にプロパティに指定した型とundefined型のユニオン型になる。

これはプロパティが存在しない時に、値がundefinedになるのを正確に型で表すため。

const obj: { [K: string]: number } = { a: 1 };
const b: number | undefined = obj.b;
console.log(b);
// undefined

Record<K, T>を用いたインデックス型

インデックス型はRecord<K, T>ユーティリティ型を用いても表現できる。
次の2つの型注釈は同じ意味になる。

let obj1: { [K: string]: number };
let obj2: Record<string, number>;

引用:インデックス型

asami-okinaasami-okina

プロトタイプベース

ここでは、JavaScriptのプロトタイプベースの概要を説明する。

オブジェクトの生成

オブジェクト指向プログラミング(OOP)では、オブジェクトを扱う。
オブジェクトを扱う以上は、オブジェクトを生成する必要がある。

しかし、オブジェクトの生成方式はOOPで統一的な決まりはない。
言語によって異なる。

言語によりオブジェクト生成の細部は異なるが、生成方法は大きく分けて「クラスベース」と「プロトタイプベース」がある。

クラスベース

JavaやPHP、Ruby、Pythonなどはクラスベースに分類される。
クラスベースでのオブジェクト生成はオブジェクトの設計図である「クラス」を用いる。

クラスに対してnew演算子を用いるなどして得られるのがオブジェクトであり、クラスベースの世界ではそれを「インスタンス」と呼ぶ。

例えば、ボタンのオブジェクトが欲しい時は、まずその設計図となるボタンクラスを作る。

class Button {
  constructor(name) {
    this.name = name;
  }
}

その上でボタンクラスに対してnew演算子を用いると、ボタンオブジェクトが得られる。

const dangerousButton = new Button("絶対に押すなよ?");

このような言語がクラスベースと言われるのは、オブジェクトの素となるのがクラスだから。

プロトタイプベースとは

一方のJavaScriptのオブジェクト生成はプロトタイプベース。
プロトタイプベースの特徴は、クラスのようなものがないところ。

クラスベースではオブジェクトの素となるのはクラスだった。
プロトタイプベースにはクラスがない。

では、何をもとにしてオブジェクトを生成するのか?
答えは、「オブジェクトを素にして新しいオブジェクトを生成する」。

例えば、JavaScriptでは既存のオブジェクトに対して、Object.create()を実行すると新しいオブジェクトが得られる。

const button = {
  name: "ボタン",
};
 
const dangerousButton = Object.create(button);
dangerousButton.name = "絶対に押すなよ?";

上の例のbuttonとdangerousButtonは異なるオブジェクトになる。
その証拠に、それぞれのnameプロパティは値が異なる。

console.log(button.name);
// "ボタン"
console.log(dangerousButton.name);
// "絶対に押すなよ?"

「プロトタイプ」とは日本語では「原型」のこと。
プロトタイプベースは単純に言ってしまえば、原型となるオブジェクトを素にオブジェクトを生成するアプローチ。

継承

継承についても、クラスベースとプロトタイプベースでは異なる特徴がある。
クラスベースでは、継承する時はextendsキーワードなどを用いてクラスからクラスを派生させ、派生クラスからオブジェクトを生成する手順を踏む。

ここにCounterクラスがある。

class Counter {
  constructor() {
    this.count = 0;
  }
 
  countUp() {
    this.count++;
  }
}

このクラスは数とそれをカウントアップする振る舞いを持っている。
このCounterクラスを継承して、リセット機能を持った派生クラスは次のResettableCounterクラスになる。

class ResettableCounter extends Counter {
  reset() {
    this.count = 0;
  }
}

このResettableCounterクラスを使うには、このクラスに対してnew演算子でオブジェクトを生成する。

counter = new ResettableCounter();
counter.countUp();
counter.reset();

以上の例でもわかるとおり、クラスベースでの継承とオブジェクトの生成はextendsとnewといった異なる言語機能になっていることが多い。

一方、プロトタイプベースのJavaScriptでは、継承もオブジェクトの生成と同じプロセスで行う。

次の例は、counterオブジェクトを継承したresettableCounterオブジェクトを作っている。

const counter = {
  count: 0,
  countUp() {
    this.count++;
  },
};
 
const resettableCounter = Object.create(counter);
resettableCounter.reset = function () {
  this.count = 0;
};

継承と言ってもプロトタイプベースでは、クラスベースのextendsのような特別な仕掛けがあるわけではなく、「既存のオブジェクトから新しいオブジェクトを作る」というプロトタイプベースの仕組みを継承に応用しているにすぎない。

クラスベース風にも書けるJavaScript

ここまでの説明で、 クラスベースに慣れしたしんだ読者の中には「JavaScriptでオブジェクト指向プログラミングをしようとすると随分と独特な書き方になるんだな」と思う方が多いかもしれない。

ここで誤解しないでほしいのは、プロトタイプベースのJavaScriptでもクラスのような書き方ができるようになっていること。

ES2015にclassやextends構文が導入されたため、近年のJavaScriptではクラスベース風の書き方が容易にできるようになっている。

次のコードはクラスベースの説明の時に提示したものだが、実はこれはJavaScriptだった。

class Counter {
  constructor() {
    this.count = 0;
  }
 
  countUp() {
    this.count++;
  }
}

まとめ

  • クラスベースはクラスをもとに新しいオブジェクトを生成するスタイル。
  • プロトタイプベースは、既存のオブジェクトから新しいオブジェクトを生成するスタイル。
  • プロトタイプベースでの継承は、特別な操作ではなく、オブジェクト生成とまったく同じプロセス。
  • JavaScriptがプロトタイプベースを採用したのは、言語をシンプルで柔軟なものにするのが狙い。

引用:プロトタイプベース

asami-okinaasami-okina

object、Object、{}の違い

TypeScriptではオブジェクトの型注釈をするとき、プロパティの型まで指定するのが一般的。

let obj: { a: number; b: number};

そういった一般的な型注釈とは異なり、プロパティの型を指定せず、ざっくり「オブジェクトであること」を型注釈する方法もある。

object型やObject型、{}型を使うもの。

let a: object;
let b: Object;
let c: {};

これらはどれもオブジェクト型の値ならどんなものでも代入可能になる型。

const a: object = {};
const b: Object = {};
const c: {} = {};

object、Object、{}の違い

object型やObject型、{}型の3つは類似する部分があるが、object型と他の2つは異なる点がある。

object型

object型はオブジェクト型の値だけが代入できる型。
JavaScriptの値はプリミティブ型かオブジェクト型かの2つに大分されるため、object型はプリミティブ型が代入できない型と言える。

let a: object;
a = { x: 1}; // OK
a = [1, 2, 3]; // OK 配列はオブジェクト
a = /a-z/; // OK 正規表現はオブジェクト

a = 1; // Error プリミティブ型はNG

Object型

Object型はインターフェース。
valueOfなどのプロパティを持つ値なら何でも代入できる。

したがって、Object型にはnullやundefinedを除くあらゆるプリミティブ型も代入できる

文字列型や数値型などのプリミティブ型は自動ボックス化により、オブジェクトのようにプロパティを持てるから。

let a: Object;
a = {}; // OK
 
// ボックス化可能なプリミティブ型OK
a = 1; // OK
a = true; // OK
a = "string"; // OK
 
// nullとundefinedはNG
a = null; // NG
a = undefined; // NG

Object型はTypeScriptの公式ドキュメントで使うべきではないとされている。
理由はプリミティブ型も代入できてしまうから。

もしオブジェクト型ならなんでも代入可にしたい場合は、代わりにobject型を検討すべき。

{}型

{}型は、プロパティを持たないオブジェクトを表す型
プロパティを持ちうる値なら何でも代入できる

この点はObjectと似ていて、nullやundefinedを除くあらゆる型を代入できる

let a: {};
a = {}; // OK
 
// ボックス化可能なプリミティブ型OK
a = 1; // OK
a = true; // OK
a = "string"; // OK
 
// nullとundefinedはNG
a = null; // Error
a = undefined; // Error

object型、Object型、{}型の代入範囲

object型やObject型、{}型の代入範囲をまとめると次の図のようになる。

引用:object型、Object型、{}型の違い

asami-okinaasami-okina

オブジェクトの分割代入

JavaScriptには、オブジェクトの分割代入という便利な構文がある。
分割代入は、オブジェクトからプロパティを取り出す機能。

通常、オブジェクトからプロパティを取り出す場合は、プロパティアクセサーを使う。
プロパティアクセサーは、ドットを使ってプロパティを参照する記法。

const item = { price: 100 };
const price = item.price;

分割代入は、中カッコ{}に取り出したいプロパティを指定することで、プロパティ名と同じ名前の変数を作ることができる

次の分割代入のサンプルコードは、上のプロパティアクセサーを使ったコードと同等の処理になる。

const item = { price: 100};
const { price } = item;

分割代入は、プロパティ名と同じ名前で変数を定義するときに、プロパティ名を2度書かないで済むのがひとつの利点。

複数のプロパティを取り出す

分割代入は、複数のプロパティを一度に取り出すこともできる。
その場合、取り出したいプロパティを中カッコに列挙する。

const obj = { a: 1, b: 2};
const { a, b } = obj;

const color = { r: 0, g: 122, b: 204, a: 1 };
const { r, g, b, a } = color;

代入する変数名の指定

オブジェクトの分割代入では、コロンの後に変数名を指定すると、その名前の変数に代入できる。

const color = { r: 0, g: 122, b: 204, a: 1};
const { r: red, g: green, b: blue, a: alha } = color;
console.log(green); // 122

入れ子構造の分割代入

オブジェクトの中にオブジェクトの入れ子構造にも、分割代入が使える。
深い階層のプロパティを取り出すには、階層の分だけ中カッコで囲む。

const continent = {
  name: "北アメリカ",
  us: {
    name: "アメリカ合衆国",
    capitalCity: "ワシントンD.C.",
  },
};
 
const {
  us: { name, capitalCity },
} = continent;
 
console.log(name);
// "アメリカ合衆国"
console.log(capitalCity);
// "ワシントンD.C."

入れ子構造の分割代入と変数名の指定

入れ子構造の分割代入をしながら、値を代入する変数名を指定することを同時にすることもできる。

const continent = {
  name: "北アメリカ",
  us: {
    name: "アメリカ合衆国",
    capitalCity: "ワシントンD.C.",
  },
};
 
const {
  name: continentName,
  us: { name: countryName },
} = continent;
 
console.log(continentName);
// "北アメリカ"
console.log(countryName);
// "アメリカ合衆国"

分割代入のデフォルト値

分割代入では、=のあとにデフォルト値が指定できる。
デフォルト値は値がundefinedの時に代入される。

const color = { r: undefined, g: 122, b: 204 };
const { r = 0, g = 0, b = 0 } = color;
console.log(r, g, b);
// 0,  122,  204

値がnullの時はデフォルト値は使われず、nullがそのまま代入される。

const color = { r: null };
const { r = 0 } = color;
console.log(r);
// null

デフォルト値と変数名の指定

分割代入のデフォルト値と代入先の変数名を同時に指定することもできる。
その場合、代入先変数名に続けてデフォルト値を書く。

const color = { r: undefined, g: 122, b: 204 };
const { r: red = 0 } = color;
console.log(red);
// 0

引用:オブジェクトの分割代入

asami-okinaasami-okina

Shorthand property names

オブジェクトのキーと変数名が同じ時に限り、オブジェクトに値を代入するときも同様にShorthand property namesを使うことができる。

type Wild = {
  name: string;
  no: number;
  genre: string;
  height: number;
  weight: number;
};
 
const name = "pikachu";
const no = 25;
const genre = "mouse pokémon";
const height = 0.4;
const weight = 6.0;
 
const pikachu: Wild = {
  name,
  no,
  genre,
  height,
  weight,
};

要するにこちらの省略型。

const pikachu: Wild = {
  name: name,
  no: no,
  genre: genre,
  height: height,
  weight: weight,
};

引用:Shorthand property names

asami-okinaasami-okina

オプショナルチェーン

JavaScriptのオプショナルチェーン?.は、オブジェクトのプロパティが存在しない場合でもエラーを起こさずにプロパティを参照できる安全な方法。

プロパティ参照がエラーになる問題

JavaScriptではnullやundefinedのプロパティを参照するとエラーが発生する。

const book = undefined;
const title = book.title;
// TypeError: Cannot read property 'title' of undefined
 
const author = null;
const email = author.email;
// TypeError: Cannot read property 'email' of null

エラーを避けるには、値がnullやundefinedでないかチェックする必要がある。

const book = undefined;
const title = book === null || book === undefined ? undefined : book.title;
console.log(title);
// undefined
 
const book = { title: "サバイバルTypeScript" };
const title = book === null || book === undefined ? undefined : book.title;
console.log(title);
// "サバイバルTypeScript"

ネストしたオブジェクトの場合、チェック処理はいっそう複雑になる。

const book = { author: { email: "alice@example.com" } };
const authorEmail =
  book === null || book === undefined
    ? undefined
    : book.author === null || book.author === undefined
    ? undefined
    : book.author.email;

オプショナルチェーン

JavaScriptのオプショナルチェーンはnullやundefinedのプロパティを誤って参照しないようにしつつ、記述量を抑えられる書き方。

const book = undefined;
const title = book?.title;
//                ^^オプショナルチェーン
console.log(title);
// undefined
 
const book = { title: "サバイバルTypeScript" };
const title = book?.title;
console.log(title);
// "サバイバルTypeScript"

オプショナルチェーンはネストして使うこともできる。

const book = undefined;
const authorEmail = book?.author?.email;
console.log(authorEmail);
// undefined
 
const book = { author: { email: "alice@example.com" } };
const authorEmail = book?.author?.email;
console.log(authorEmail);
// "alice@example.com"

もし**?.に先行する変数やプロパティの値がnullまたはundefinedの時は、その先のプロパティは評価されず、「undefined」が返る**。

const book = null;
console.log(book?.title);
// undefined
 
const book = { author: null };
console.log(book.author?.name);
// undefined

関数呼び出し

関数を呼び出す時にもオプションチェーンが使える。
関数に使う場合は、引数括弧の前に?.を書く。

const increment = undefined;
const result = increment?.(1);
console.log(result);
// undefined
 
const increment = (n) => n + 1;
const result = increment?.(1);
console.log(result);
// 2

メソッドを呼び出すときも同様の書き方。

const book = { getPrice: undefined };
console.log(book.getPrice?.());
undefined
 
const book = {
  getPrice() {
    return 0;
  },
};
console.log(book.getPrice?.());
// 0

配列要素の参照

配列要素を参照する際にもオプショナルチェーンが使える。
要素を参照する場合は、括弧の前に?.を書く。

const books = undefined;
const title = books?.[0];
console.log(title);
// undefined
 
const books = ["サバイバルTypeScript"];
const title = books?.[0];
console.log(title);
// "サバイバルTypeScript"

TypeScriptでの型

TypeScriptでオプショナルチェーンを使った場合、得られる値の型は最後のプロパティの型とundefinedのユニオン型となる。

let book: undefined | { title: string };
const title = book?.title;
// const title: string | undefined

Null合体演算子と組み合わせる

オプショナルチェーンがundefinedを返した時に、デフォルト値を代入したい場合がある。
その際には、Null合体演算子??を用いると便利

const book = undefined;
const title = book?.title ?? "デフォルトタイトル";
console.log(title);
// "デフォルトタイトル"

引用:オプショナルチェーン

asami-okinaasami-okina

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

JavaScript, TypeScriptでオブジェクトのプロパティをループする方法を説明する。

for-in文

JavaScriptでオブジェクトをループする古くからある方法はfor-in文を使うもの。

const foo = { a: 1, b: 2, c: 3 };
for (const prop in foo) {
  console.log(prop, foo[prop]);
  // a 1
  // b 2
  // c 3 の順で出力される
}

for-in文ではhasOwnPropertyを使おう

JavaScriptのオブジェクトにはもとになるプロパティタイプがある。

const foo = { a: 1, b: 2, c: 3 };
console.log(Object.getPrototypeOf(foo) === Object.prototype);
// true

Object.prototypeを変更するとその影響は、このプロトタイプを持つすべてのオブジェクトに影響する。

const foo = { a: 1 };
const date = new Date();
const arr = [1, 2, 3];
 
// どのオブジェクトもhiプロパティが無いことを確認
console.log(foo.hi, date.hi, arr.hi);
// undefined undefined undefined
 
// プロトタイプにプロパティを追加する
Object.prototype.hi = "Hi!";
 
// どのオブジェクトもhiプロパティを持つようになる
console.log(foo.hi, date.hi, arr.hi);
// Hi! Hi! Hi!

for-in文はプロトタイプのプロパティも含めてループする仕様がある。
そのため、プロトタイプが変更されると、意図しないところでfor-inのループの回数が変わることがある。

const foo = { a: 1, b: 2, c: 3 };
Object.prototype.hi = "Hi!";
for (const prop in foo) {
  console.log(prop, foo[prop]);
  // a 1
  // b 2
  // c 3
  // hi Hi! の順で出力される
}

したがって、for-inで反復処理を書く場合は、hasOwnPropertyでプロパティがプロトタイプのものでないことをチェックした方が安全。

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.entriesの戻り値をfor-of文でループする方法もある。

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 の順で出力される
}

for-in文と異なり、hasOwnPropertyのチェックが不要。

Object.keys

プロパティのキーだけを反復処理する場合は、Object.keyの戻り値をfor-of文でループする方法がある。

const foo = { a: 1, b: 2, c: 3 };
for (const key of Object.keys(foo)) {
  console.log(key);
  // a
  // b
  // c の順で出力される
}

for-in文と異なり、hasOwnPropertyのチェックが不要。

Object.values

プロパティの値だけを反復処理する場合は、Object.valuesの戻り値をfor-of文でループする方法がある。

const foo = { a: 1, b: 2, c: 3 };
for (const value of Object.values(foo)) {
  console.log(value);
  // 1
  // 2
  // 3 の順で出力される
}

for-in文と異なり、hasOwnPropertyのチェックが不要。

引用:オブジェクトをループする方法

asami-okinaasami-okina

構造的部分型

オブジェクト指向型。
そこまでとはいかなくてもクラスを扱うことができる言語において、ある元となる型(基本型)とその継承関係にある型(派生型)という話は欠かすことができない。

構造的部分型とは

プログラミング言語の派生型の方式は公称型と構造的部分型の2種類が存在する。
どちらも派生型の定義としてリスコフの置換原則を満たしており、どちらが正しいというものではない。

※リスコフの置換原則
継承に関する原則で、親クラスのインスタンスが適用されるコードに対して、子クラスのインスタンスで置き換えても問題なく動くべきだという原則。簡単にいうと、親クラスの振る舞いを子クラスが勝手に壊してはいけないという原則。

それぞれが正しい、異なる派生型の解釈。

公称型と構造的部分型の違いを理解しておくことでより安全で堅牢なTypeScri[tができるようになる。

公称型

Java, C++で採用されている定義。
ある型を基本型にする派生型は互いに置換できない。

構造的部分型

Go, TypeScriptで採用されている定義。
その型の見た目が等しければ置換可能であるという点が公称型と大きく異なる。
公称型ほど硬くはなく、とはいえ型の恩恵は受けたいというややゆるい型つけ。

実際の例で見る

データを意味するクラスのDataを、ファイル読み込みで取得する場合と他のサーバーにリクエストを送信して取得する場合を考える。

InputSourceというスーパークラス(基本型)を考え、そのサブクラス(派生型)としてFile, Requestを考える。

Dataクラスの構造は重要ではないため、欲しいデータがそのような形をしている程度に考えて。

なお、登場するクラスはほとんどは実際に存在せず、理解のために英語をあたかもクラスのように書いており、そのままコードを転記しても動かないよ。

公称型の場合

以下はJavaでの紹介。

public class InputSource {
  public Data fetch() {
    throw new UnsupportedOperationException("Please implement InputSource and override this method");
  }
}
public class File extends InputSource {
  public final String destination;
  public File(final String destination) {
    this.destination = destination;
  }
  public Data fetch() {
    final Reader reader = FileSystem.readFrom(destination);
    // ...
    return data;
  }
}
public class Request extends InputSource {
  public final String destination;
  public Request(final String destination) {
    this.destination = destination;
  }
  public Data fetch() {
    final Response response = HTTPRequest.get(destination);
    // ...
    return data;
  }
}

このようなクラスの関係であればリスコフの置換原則から次のようにして動かすことができる。
これはFile, Requestのインスタンスをスーパークラスの変数で受けていることを意味する。

final InputSource source1 = new File("/data/~~~.txt");
final InputSource source2 = new Request("https://~~~");
final Data data1 = source1.fetch();
final Data data2 = source2.fetch();

※Javaのスーパークラス
親クラス、基底クラスとも呼ばれる。
継承関係において、自身のフィールドやメソッドを派生クラス(子クラス)に継承するクラスのことを指す。

次のように結果を受ける変数の方をお互いのサブクラスに変更する。

final Request source3 = new File("/data/~~~.txt");
final File source4 = new Request("https://~~~");

すると、このようなエラーが得られる。

incompatible types: File cannot be converted to Request
  final Request source3 = new File("/data/~~~.txt");
                          ^
incompatible types: Request cannot be converted to File
  final File source4 = new Request("https://~~~");
                       ^

これは公称型に慣れ親しんでいる方にとっては至極当然の結果。
FileはRequestではなく、RequestはFileではないため入れ替えることはできない。

構造的部分型の場合

以下はTypeScriptでの紹介。

class File extends InputSource {
  public readonly destination: string;
 
  public constructor(destination: string) {
    super();
    this.destination = destination;
  }
 
  public fetch(): Data {
    const reader: Reader = FileSystem.readFrom(this.destination);
    // ...
 
    return data;
  }
}
 
class Request extends InputSource {
  public readonly destination: string;
 
  public constructor(destination: string) {
    super();
    this.destination = destination;
  }
 
  public fetch(): Data {
    const response: Response = HTTPRequest.get(this.destination);
    // ...
 
    return data;
  }
}

こちらも同様にリスコフの置換原則が成立するのでスーパークラスの変数でサブクラスを受けることができる。

const source1: InputSource = new File("/data/~~~.txt");
const source2: InputSource = new Request("https://~~~~");
 
const data1: Data = source1.fetch();
const data2: Data = source2.fetch();

次に、先ほどと同じように結果を受ける変数の型をお互いのサブクラスに変更してみる。

const source3: Request = new File("/data/~~~.txt");
const source4: File = new Request("https://~~~~");
 
const data3: Data = source3.fetch();
const data4: Data = source4.fetch();

これはエラーが出ることなく実行できる。
これが構造的部分型の大きな特徴で、File、Requestのしぐにちゃが同じために可能となる。

interface InputSource {
  destination: string;
 
  fetch(): Data;
}

File, Requestは共にこのInputSourceのようなインターフェースであると解釈されるためこのようなことが起こる。

TypeScriptでさらに注意すること

今回の例はともに同じスーパークラスを持つサブクラスの話だったが、実はこれはスーパークラスが異なっていても起こり得る

スーパークラスのInputSourceを上記TypeScriptの例から抹消してしまっても同様にこのコードは動作する。

class File {
  public destination: string;
 
  public constructor(destination: string) {
    this.destination = destination;
  }
 
  public fetch(): Data {
    const reader: Reader = FileSystem.readFrom(this.destination);
    // ...
 
    return data;
  }
}
 
class Request {
  public destination: string;
 
  public constructor(destination: string) {
    this.destination = destination;
  }
 
  public fetch(): Data {
    const response: Response = HTTPRequest.get(this.destination);
    // ...
 
    return data;
  }
}
 
const source3: Request = new File("/data/~~~.txt");
const source4: File = new Request("https://~~~~");
 
const data3: Data = source3.fetch();
const data4: Data = source4.fetch();

消えたのはInputSourceと、その継承を示すextends InputSourceとsuper();だけ。
このコードは正常なTypeScriptのコードとして動作する。

引用:構造的部分型

asami-okinaasami-okina

配列リテラル

JavaScriptでは配列を配列リテラルで書ける。
配列リテラルはブラケット[]を用いて書く。

[1, 2, 3];

配列リテラルは要素の区切れ目で改行して書くこともできる。
最後の要素にはカンマを書いても良い。

[
  1,
  2,
  3,
]

引用:配列リテラル

asami-okinaasami-okina

配列の型注釈

TypeScriptでは、配列に型注釈する方法が2通りある。

Type[]

1つ目の型注釈は、要素の型の後ろに[]をつける書き方。
たとえば、数値型の配列の型注釈はnumber[]と書く。

let array = number[];
array = [1, 2, 3];

Array<T>

2つ目の型注釈は、Array<T>を用いる書き方。
Tには要素の型を書く。
例えば数値型の配列の型注釈はArray<number>と書く。

let array: Array<number>;
array = [1, 2, 3];

Type[]とArray<T>どちらを使うべきか?

コンパイラのチェックの内容はどちらも同じなので、書き手の好み。

引用:配列の型注釈

asami-okinaasami-okina

配列はオブジェクト

JavaScriptの配列はオブジェクト。
そのため、比較やコピーの際の挙動に注意が必要。

配列同士の比較

配列の中身が同じでも、オブジェクトのインスタンスが異なると==では期待する比較ができない。

const list1 = [1, 2, 3];
const list2 = [1, 2, 3];
console.log(list1 == list2);
false

配列の中身を調べるための比較演算子やメソッドはJavaScriptにはないため、中身を比較したい時はlodashのisEqualなどのパッケージを使うのがお勧め。

配列のコピー

配列も他のオブジェクトと同様に、代入を用いても値のコピーにはならない。
代入元の変数と代入先の変数は同じ値を指す

そして、一方の変数だけを更新したつもりでも、他方にも変更が反映される。

const arr = [1, 2, 3];
const backup = arr;
arr.push(4); // 変更
console.log(arr);
// [1, 2, 3, 4]
console.log(backup); // こちらにも影響
// [1, 2, 3, 4]

単純な配列のコピーにはスプレッド構文を使ってね。

const arr = [1, 2, 3];
const backup = [...arr]; // スプレッド構文
arr.push(4); // 変更
console.log(backup); // 影響なし
//  [1, 2, 3]

引用:配列はオブジェクト

asami-okinaasami-okina

配列要素へのアクセス

Javascriptの配列の要素にアクセスするにはブラケット[]を使う。
ブラケットにはアクセスする要素のインデックス番号を書く。

インデックス番号は0始まり。

const abc = ["a", "b", "c"];
console.log(abc[0]);
// "a"

JavaScriptの配列では、存在しないインデックス番号でもアクセスできる。
その場合でも、JavaScriptではエラーにならず、得られる値はundefinedになる。

const abc = ["a", "b", "c"];
console.log(abc[100]);
// undefined

TypeScriptの要素の型

TypeScriptでは、Type[]型の配列から要素を取り出した時、その値の方はTypeになる。
例えば、string[]型から0番目の要素の型はstringになる。

const abc: string[] = ["a", "b", "c"];
const character: string = abc[0];

TypeScriptでも不在要素へのアクセスはコンパイラーが警告することはない。

const abc = ["a", "b", "c"];
const character: string = abc[100]; // エラーにはならない

要素アクセスで得た値はstringとundefinedどちらの可能性もありながら、TypeScriptは常にstring型であると考えるようになっている。

そのため、要素アクセスでundefinedが返ってくる場合のエラーTypeScriptでは発見できず、JavaScript実行時に判明することになる。

TypeScriptで要素アクセスを型安全にする方法

TypeScriptにこの問題を指摘してもらうようにするには、コンパイラーオプションのnoUnckeckedIndexedAccessを有効にする。

これを有効にすると、例えばstring[]配列から要素アクセスで得た値の型はstringもしくはundefined型を意味するstring | undefinedになる。

const abc: string[] = ["a", "b", "c"];
const character: string | undefined = abc[0];
character.toUpperCase(); // Object is possibly 'undefined'.

string | undefined型のままではtoUpperCaseなどの文字列型のメソッドは呼び出せない。
そこでif文で文字列型だけになるように絞り込む

すると、文字列型のメソッドを呼び出してもコンパイルエラーで指摘されることがなくなる。

const abc: string[] = ["a", "b", "c"];
const character = abc[0];
// 絞り込み条件
if (typeof character === "string") {
  character.toUpperCase(); // コンパイルエラーにならない
}

配列要素へのアクセスを安全にするために、noUncheckedIndexedAccessを有効にしておくことが推奨される。

引用:配列要素へのアクセス

asami-okinaasami-okina

配列の分割代入

JavaScriptでは、配列から要素を取り出す方法のひとつに、array[1]のようにインデックスでアクセスする方法がある。

この方法とは別に、分割代入という方法を使っても配列要素をアクセスできる。

例えば、[1, 2, 3, 4, 5]のような配列から、最初の3要素を取り出して変数に代入するには次のように書く。

const oneToFive = [1, 2, 3, 4, 5];
const [one, two, three] = oneToFive;
console.log(one);
// 1
console.log(two);
// 2
console.log(three);
// 3

存在しない要素に対して分割代入した場合は、変数にundefinedが代入され、JavaScriptではエラーにならない。

const oneToFive = [1, 2];
const [one, two, three] = oneToFive;
console.log(three);
// undefined

TypeScriptでは分割代入された値の型はT[]の配列ならT型になる。
たとえば、number[]型の[1, 2, 3, 4, 5]から分割代入したのなら、型はnumberになる。

const oneToFive = [1, 2, 3, 4, 5];
const [one, two, three] = oneToFive;
const num: number = one; // oneはnumber型になるので代入できる

ただしTypeScriptのコンパイラーオプションnoUncheckedIndexedAccessを有効にした場合は異なる。

このオプション有効状態で配列T[]から分割代入するとT型もしくはundefined型を示すT | undefined型になる。

たとえば、number[]型の[1, 2, 3, 4, 5]から分割代入したのなら、型はnumber | undefinedになる。

const oneToFive = [1, 2, 3, 4, 5];
const [one, two, three] = oneToFive;
const num: number = one;
// 上はコンパイルエラーになる。
// oneはnumber | undefinedになり、numberには代入できないため。

ネストした配列の分割代入

JavaScriptの分割代入はフラットな配列だけでなく、ネストした入れ子構造の配列からも要素を抽出できる。

ネストした要素の分割代入の書き方は、ネスト構造と一致するようにブラケットを重ねる。

const twoByTwo = [
  [1, 2],
  [3, 4],
];
const [[one, two], [three]] = twoByTwo;
console.log(one);
// 1
console.log(two);
// 2
console.log(three);
// 3

途中要素の分割代入

配列の分割代入は先頭からでなく、途中の要素を取り出すこともできる。
その場合、取り出さない要素の数だけカンマを書く。

const oneToFive = [1, 2, 3, 4, 5];
const [, , , four, five] = oneToFive;
console.log(four);
// 4
console.log(five);
// 5

残余部分の代入

JavaScriptの配列を分割代入するときに、残余パターン(...)を用いて、配列の残りの部分を取り出して変数に代入できる。

const oneToFive = [1, 2, 3, 4, 5];
const [one, ...rest] = oneToFive;
console.log(one);
// 1
console.log(rest);
// [ 2, 3, 4, 5 ]

このとき、TypeScriptでは残余部分の型は配列のnumber[]になる。

引用:配列の分割代入

asami-okinaasami-okina

配列の破壊的操作

JavaScriptの配列メソッドには、破壊的なメソッドと非破壊的なメソッドの2種類がある。
特に破壊的なメソッドは注意深く使う必要がある。

破壊的なメソッド

非破壊的なメソッドは、操作に配列の変更を伴わないメソッド。
例えば、concatは非破壊的なメソッド。

これは複数の配列を結合するメソッドで、元の配列は書き換えず、新しい配列を返す。

const nums1 = [1, 2];
const nums2 = [3, 4];
const all = nums1.concat(nums2);
console.log(nums1);
// [ 1, 2 ]
console.log(nums2);
// [ 3, 4 ]
console.log(all);
// [ 1, 2, 3, 4 ]

非破壊的なメソッドの一覧

  • concat: 2つ以上の配列を結合した配列を返す
  • find: 提供されたテスト関数を満たす配列内の最初の要素を返す
  • findIndex: 配列内の指定されたテスト関数を満たす最初の要素の位置を返す
  • lastIndexOf: 配列中で与えられた要素が見つかった最後のインデックスを返す
  • slice: 配列の一部を切り出して返す
  • includes: 配列に任意の要素が含まれているかをtrueかfalseで返す
  • join: 全要素を連結した文字列を返す
  • keys: 配列のインデックスをArray Iteratorオブジェクトで返す
  • entries: 配列のインデックスと値のペアをArray Iteratorオブジェクトで返す
  • values: 配列の値をArray Iteratorオブジェクトで返す
  • forEach: 与えられた関数を配列の各要素に対して一度ずつ実行する
  • filter: 与えられた関数によって実装されたテストに合格したすべての配列からなる新しい配列を返す
  • flat: すべてのサブ配列の要素を指定した深さで再帰的に結合した新しい配列を返す
  • flatMap: 最初にマッピング関数を使用してそれぞれの要素をマップした後、結果を新しい配列内にフラット化する。
  • map: 与えられた関数を配列のすべての要素に対して呼び出し、その結果から新しい配列を返す
  • every: 配列内のすべての要素が指定された関数で実装されたテストに合格するかどうかをテストする
  • some: 配列の少なくとも一つの要素が指定された関数で実装されたテストに合格するかどうかをテストする
  • reduce: 配列のそれぞれの要素に対してユーザーが提供した「縮小」コールバック関数を呼び出す。
  • reduceRight: アキュームレーターと配列のそれぞれの値に対して右から左へ関数を適用して単一の値にする
    引用:配列の破壊的操作

破壊的なメソッド

破壊的なメソッドは、配列の内容や配列の要素の順番を変更する操作をともなうメソッド。
例えば、pushは破壊的メソッドの1つ。

破壊的なメソッドの一覧

  • push: 配列の末尾に要素を追加する
  • unshift: 配列の最初に要素を追加する
  • pop: 配列の最後の要素を取り除き、その要素を返す
  • shift: 配列から最初の要素を取り除き、その要素を返す
  • splice: 要素を取り除いたり、置き換えたり、新しい要素を追加する
  • sort: 配列の要素をソートする
  • reverse: 配列の要素を逆順に並び替える
  • fill: 開始インデックスから終了インデックスまでのすべての要素を静的な値に変更した配列を返す
  • copyWithin: サイズを変更せずに、配列の一部を同じ配列内の別の場所にシャローコピーして返す

特に要注意な破壊的なメソッド

reverseメソッドは配列を逆順にした配列を返す。
戻り値があるので一見すると非破壊的なメソッドに見えなくもない。
しかし、このメソッドは配列の順番も逆にしてしまうので注意が必要。

const nums = [1, 2, 3];
const newNums = nums.reverse();
console.log(nums);
// [ 3, 2, 1 ]
console.log(newNums);
// [ 3, 2, 1 ]

破壊的なメソッドを安全に使う方法

破壊的なメソッドを非破壊的に使うには、破壊的操作を行う前に、配列を別の配列にコピーする。

配列のコピーはスプレッド構文...を用いる。
コピーした配列に対して破壊的操作を行えば、元の配列が変更される心配がなくなる。

const original = [1, 2, 3];
const copy = [...original]; // コピーを作る
copy.reverse();
console.log(original); // 破壊的操作の影響がない
// [ 1, 2, 3 ]
console.log(copy);
// [ 3, 2, 1 ]

このreverseの例は、コピーと破壊的なメソッドの呼び出しを1行に短縮して書くこともできる。

const original = [1, 2, 3];
const reversed = [...original].reverse();
console.log(original);
// [ 1, 2, 3 ]
console.log(reversed);
// [ 3, 2, 1 ]

引用:配列の破壊的操作

asami-okinaasami-okina

配列をループする方法

JavaScript、TypeScriptで配列をループするには、主にfor文、for-of文、配列のメソッドの3つの方法がある。

for文

for文は古くからある配列をループする方法。

const arr = ["a", "b", "c"];
for (let i = 0; i < arr.length; i++) {
  console.log(i, arr[i]);
  // 0 a
  // 1 b
  // 2 c の順で出力される
}

breakでループを中断できる。

const arr = ["a", "b", "c"];
for (let i = 0; i < arr.length; i++) {
  console.log(arr[i]);
  if (arr[i] === "b") {
    break;
  }
}
// a b が順に出力されて終了する

continueで次のループにスキップできる。

const arr = ["a", "b", "c"];
for (let i = 0; i < arr.length; i++) {
  if (arr[i] === "a") {
    continue;
  }
  console.log(arr[i]);
  // b c が順に出力される
}

for-of文

for-of文は、配列の要素をひとつひとつ処理する場合、for文よりもシンプルに書けるのが特徴。

const arr = ["a", "b", "c"];
for (const value of arr) {
  console.log(value);
  // a b cの順で出力される
}

for-of文もfor文と同様に、breakやcontinueが使える。

Arrayのメソッド

Arrayには要素ごとに処理を行うメソッドがいくつかある。

forEachメソッドに渡したコールバック関数が、要素ごとに実行される。
forEachには戻り値がない。

for文などと異なり、breakやcontinueは使えない。

const arr = ["a", "b", "c"];
arr.forEach((value, i) => {
  console.log(value, i);
  // a 0
  // b 1
  // c 2 の順で出力される
});

mapメソッドも要素ごとにコールバック関数を実行する。
コールバック関数の戻り値がmapには戻り値になる。

配列要素の値を加工して、別の配列を作るときに便利。
mapではbreakやcontinueは使えない。

const arr = ["a", "b", "c"];
const arr2 = arr.map((value) => value + value);
console.log(arr2);
// [ 'aa', 'bb', 'cc' ]

for-in文は使わない

for-in文で配列をループすることもできる。
しかし、for-in文は配列をループするのには使わないほうがよい。

配列は順番が重要なことが多いが、for-in文は順番通りになる保証がないため。

また、配列オブジェクトに追加のプロパティがある場合、for-in文はそれも反復処理に含める。
これが予期しない不具合につながる危険性もある。

const arr = ["a", "b", "c"];
arr.foo = "bar"; // 追加のプロパティ
for (const x in arr) {
  console.log(x, arr[x]);
  // 0 a
  // 1 b
  // 2 c
  // foo bar が順に出力される
}

引用:配列をループする方法

asami-okinaasami-okina

配列のスプレッド構文「...」

JavaScriptの配列ではスプレッド構文「...」を使うことで、要素を展開することができる。

配列の作成

ある配列に要素を追加して新しい配列を作成する場合に、スプレッド構文を使わない場合は次のように書く必要がある。

const arr = [1, 2, 3];
const arr2 = [];
for (const item of arr) {
  arr2.push(item);
}
arr2.push(4);

スプレッド構文を使用することで、上の実装は次のように簡単に書き直すことができる。

const arr = [1, 2, 3];
const arr2 = [...arr, 4];

スプレッド構文は配列リテラルの好きな位置に記述できるので、要素と要素の間に他の配列を挿入することもできる。

const arr = [1, 2, 3];
const arr2 = [0, ...arr, 4];

配列のコピー

配列のコピーを作る際に、スプレッド構文が便利な場合がある。
スプレッド構文で作成されたコピーは、元の配列とは異なる実体を持つ。

const arr = [1, 2, 3];
const backup = [...arr];
arr.push(4); // 変更を加える
console.log(arr);
// (4) [1, 2, 3, 4]
console.log(backup); // コピーには影響なし
// (3) [1, 2, 3]

注意点として、この方法は浅いコピー。
深いコピーではない点に注意。

浅いコピーで複製できるのは、1層目の要素だけ。
配列の中に配列が入っている場合は、2勝目より深くある配列は元の配列のものと値を共有する。

const arr = [1, [2, 3]];
const backup = [...arr];
arr[1].push(4);
console.log(arr[1]);
// (3) [2, 3, 4]
console.log(backup[1]); // 変更の影響あり
// (3) [2, 3, 4]

スプレッド演算子と同等の手段として、配列のconcatメソッドを用いる方法もある。

配列の連結

配列の連結もスプレッド構文を使用して簡単に書ける。

const arr = [1, 2, 3];
const arr2 = [4, 5, 6];
const concated = [...arr, ...arr2];

引用:配列のスプレッド構文「...」

asami-okinaasami-okina

配列の共変性

TypeScriptの配列の型は共変。
ここでは配列の共変性がどのようなものなのか、共変性があるためにどういうことに注意が必要なのか、なぜTypeScriptの配列は共変なのかについて見ていく。

共変とは

型の世界の話で、共変とはその型自身、もしくはその部分型が代入できることを言う。
例えば、Animal型とDog型の2つの型があるとする。

DogはAnimalの部分型とする。
共変であれば、Animal型の変数にはAnimal自身とその部分型のDogが代入できる。

interface Animal {
  isAnimal: boolean;
}
interface Dog extends Animal {
  isDog: boolean;
}
 
let pochi: Dog = { isAnimal: true, isDog: true };
let animal: Animal = pochi; // 代入OK

一方で共変では、Dog型の変数には、DogのスーパータイプであるAnimalは代入できない

let animal: Animal = { isAnimal: true };
let pochi: Dog = animal; // Error

配列は共変が許される

TypeScriptの配列型は共変である。
たとえば、Animal[]型の配列にDog[]を代入できる。

const dogs: Dog[] = [pochi];
const animals: Animal[] = dogs; // 代入OK

一見するとこの性質は問題なさそうだが、次の例のようにanimals[0]をAnimal型の値に置き換えると問題が起こる。

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();
// JS実行時エラー: mayBePochi.wanwan is not a function

変数animalsにdogを代入した場合、animalsの変更はdogsにも影響する。
これはJavaScriptの配列がミュータブルなオブジェクトであるため。

animals[0]にAnimal型を代入すると、dog[0]もAnimalの値になる。
dogはDog[]型のため型どおりならAnimal型を受け付けないことが望ましいが、実際はそれができてしまう。

その結果、dogs[0]のwanwanメソッドを呼び出すところでメソッドが存在しないというJavaScript実行時エラーが発生する。

型の安全性を突き詰めると、配列は共変であるべきではない。
型がある他の言語のJavaでは、List<T>型は共変ではなく非変になっている。

非変な配列では、その型自身しかだいにゅうできないようになり、上のような問題が起こらない。

TypeScriptで共変になっている理由

配列が非変である言語がある中、TypeScriptはなぜ型の安全性を犠牲にしてまで配列を共変にしているのか。

それはTypeScriptが健全性と利便性のバランスをとることを目標にして、型システムを設計しているため。

配列が非変であると健全性は高くなるが、利便性は下がる。

では、具体的にはどのようなところで不便になるのか見ていこう。
込み入った話になるため、段階を踏んで説明していく。

まず、共変とはある型とその部分型が代入できること。
たとえば、number型はユニオン型のnumber | null型の部分型となる。

これを配列にしたnumber[]型は、(number | null)[]型の部分型ということになる。

TypeScriptの配列の型は共変。
したがって、number[]型は(number | null)[]型に代入できる。

もし、TypeScriptの配列の型が非変なら、(number | null)[]型に代入できるのは、それ自身になる。つまり、number[]は(number | null)[]に代入できないことになる。

ここまでのことを整理すると次のようになる。

  • numberはnumber | nullの部分型
  • number[]は(number | null)[]の部分型
  • 共変なら、(number | null)[]にnumber[]が代入できる
  • 非変なら、(number | null)[]にnumber[]は代入できない

次に、ここで話を変えて、次のような関数を考えてみる。

function sum(values: (number | null)[]): number {
  let total = 0;
  for (const value of values) {
    if (typeof value === "number") {
      total += value;
    }
  }
  return total;
}

このsum関数は、(number | null)[]、つまり数値とヌルが混在しうる配列を受け取り、数値だけピックアップして、その合計値を返す関数。

関数の引数に代入する場合も、TypeScriptの配列は共変。
共変なので、次のようなnumber[]型の値を代入できる。

const values: number[] = [1, 2, 3];
sum(values);

もしも、TypeScriptの配列が非変だと、上のようなコードはコンパイルエラーになるだろう。
sum関数は、引数に(number | null)[]を期待していますが、number[]を渡しているからである。

そして、そのようなコンパイルエラーを回避しようとしたら、次のような余計な型アサーションを加えたりしないといけない。

sum(values as (number | null)[]);
//         ^^^^^^^^^^^^^^^^^型アサーション

こうしたことが随所で起きると、書くのも読むのも不便になる。
したがって、TypeScriptでは型の完璧さよりも、利便性を優先しているものと考えられる。

引用:配列の共変性

asami-okinaasami-okina

タプル

TypeScriptの関数は1値のみ返却可能。
戻り値に複数の値を返したい場合、配列に返したいすべての値を入れて返すことがある。

なお、次の関数の戻り値は定数になっているが、実際は演算した結果だと解釈してください。

function tuple() {
  //...
  return [1, "ok", true];
}

配列が抱える問題

上記例では戻り値の型として何が妥当だろうか。

なんでも入れられる型、ということでany[]またはunknown[]が型の候補として思い浮かぶ人もいるかと思います。

const list: unknown[] = tuple();
 
list[0].toString(); // Error

だが、list[n]からメソッドを呼ぶことはできない。
これはlistの各要素はunknownであるから。
そこで使えるのがタプル。

タプルの型

タプルの型は簡単で[]で書いて中に型を書くだけ。
つまり、上記関数tuple()は次のような戻り値を持っていると言える。

const list: [number, string, boolean] = tuple();

同様に関数の戻り値にも書くことができる。

function tuple(): [number, string, boolean] {
  //...
  return [1, "ok", true];
}

配列の型はT[]とArray<T>のふたつの書き方があったが、タプルはこの書き方しか存在しない。

タプルへのアクセス

タプルを受けた変数はそのまま中の型が持っているプロパティ、メソッドを使用できできる。

const list: [number, string, boolean] = tuple();
 
list[0].toExponential();
list[1].length;
list[2].valueOf();

分割代入を使ってタプルにアクセスする

上記関数tuple()の戻り値は分割代入を使うと次のように受けることができる。

const [num, str, bool]: [number, string, boolean] = tuple();

また、特定の戻り値だけが必要である場合は変数名を書かず,だけを書く。

タプルを使う場面

TypeScriptで非同期プログラミングをする時に、時間のかかる処理を直列ではなく並列で行いたい時がある。

そのときTypeScriptではPromise.all()というものを使用する。
このときタプルが役に立つ。

const promise: Promise<number> = yyAsync();
const num: number = await promise;

たとえば次のような処理に時間が3秒、5秒かかる関数takes3Seconds(), takes5Seconds()があるとする。

async function takes3Seconds(): Promise<string> {
  // ...
  return "finished!";
}
 
async function takes5Seconds(): Promise<number> {
  // ...
  return -1;
}

この関数をそのまま実行すると3 + 5 = 8秒かかってしまう。

const str: string = await takes3Seconds();
const num: number = await takes5Seconds();

これをPromise.all()を使うことで次のように書くことができる。
このときかかる時間は関数の中でもっとも時間がかかる関数、つまり5秒。

const tuple: [string, number] = await Promise.all([
  takes3Seconds(),
  takes5Seconds(),
]);

このときPromise.all()の戻り値を受けた変数tupleは[string, number]。
実行する関数のPromise<T>のジェネリクスの部分とタプルの型の順番は一致する。

つまり次のように入れ替えたら、入れ変えた結果のタプルである[number, string]が得られる。

const tuple: [number, string] = await Promise.all([
  takes5Seconds(),
  takes3Seconds(),
]);

Promise.all()は先に終了した関数から順番に戻り値のタプルとして格納されることはなく、元々の順番を保持する。

take3seconds()の方が早く終わるから、先にタプルに格納されるということはなく、引数に渡した順番のとおりにタプルtupleの要素の型は決まる。

引用:タプル

asami-okinaasami-okina

列挙型(enum)

TypeScriptでは、列挙型(enum)を用いると、定数のセットに意味を持たせたコード表現ができます。

列挙型を宣言するには、enumキーワードの後に列挙型名とメンバーを書く。
次の例では、Positionが列挙型名で、Top、Right、Bottom、Leftがメンバーになる。

enum Position {
  Top,
  Right,
  Bottom,
  Left,
}

enumキーワードはTypeScript独自のもの。
そのためTypeScriptにコンパイルすると次のようなコードになる。

var Position;
(function (Position) {
    Position[Position["Top"] = 0] = "Top";
    Position[Position["Right"] = 1] = "Right";
    Position[Position["Bottom"] = 2] = "Bottom";
    Position[Position["Left"] = 3] = "Left";
})(Position || (Position = {}));

列挙型のメンバーはオブジェクトのプロパティーになる。
値は0からの連番になる。

console.log(Position.Top); // 0
console.log(Position.Right); // 1
console.log(Position.Bottom); // 2

列挙型名は型として扱うことができる。

let position: Position;

引用:列挙型(enum)

asami-okinaasami-okina

数値列挙型

TypeScriptの数値列挙型はもっとも典型的な列挙型です。
メンバーの値は上から順に0からの連番になる。

enum Position {
  Top, // 0
  Right, // 1
  Bottom, // 2
  Left, // 3
}

メンバーは値を代入できる。
値を代入した場合、それに続くメンバーは連番になる。

enum Position {
  Top = 1, // 1
  Right, // 2
  Bottom, // 3
  Left, // 4
}

引用:数値列挙型

asami-okinaasami-okina

文字列列挙型

TypeScriptの列挙型では、メンバーの値に文字列も使える。
文字列で構成された列挙型は文字列列挙型と呼ばれる。

enum Direction {
  Up = "UP",
  Down = "DOWN",
  Left = "LEFT",
  Right = "RIGHT",
}

引用:文字列列挙型

asami-okinaasami-okina

列挙型(enum)の問題点と代替手段

TypeScriptの列挙型にはいくつか問題点が指摘されている。
ここではその問題点と代替手段を説明する。

列挙型の問題点

列挙型はTypeScript独自すぎる

TypeScriptはJavaScriptを拡張した言語。
拡張といってもむやみに機能を足すのではなく、追加するのは型の世界に限って。

TypeScriptの列挙型に目を向けると、構文もJavaScriptに無いものであるだけでなく、コンパイル後の列挙型はJavaScriptのオブジェクトに変化したりと、型の世界の拡張からはみ出している独自機能になっている。

数値列挙型には型安全上の問題がある

数値列挙型はnumber型ならなんでも代入できるという型安全上の問題点がある。
次の例は値が0と1のメンバーだけからなる列挙型だが、実際にはそれ以上の数値を代入できてしまう。

enum ZeroOrOne {
  Zero = 0,
  One = 1,
}
const zeroOrOne: ZeroOrOne = 9; // コンパイルエラーは起きない

列挙型には、列挙型オブジェクトに値でアクセスすると、メンバー名を得られる仕様がある。
これにも問題があり、メンバーに無い値でアクセスしたら、コンパイルエラーになってほしいところだがそうはならない。

enum ZeroOrOne {
  Zero = 0,
  One = 1,
}
 
console.log(ZeroOrOne[0]); // これは期待どおり
// "Zero"
console.log(ZeroOrOne[9]); // これはコンパイルエラーになってほしいところ…
// undefined

文字列列挙型だけ公称型になる

TypeScriptの型システムは構造体部分型を採用している。
ところが、文字列列挙型は例外的に公称型となる。

enum StringEnum {
  Foo = "foo",
}
const foo1: StringEnum = StringEnum.Foo; // コンパイル通る
const foo2: StringEnum = "foo"; // コンパイルエラーになる

列挙型の代替案

列挙型の代替案をいくつか提示するが、列挙型の特徴を100パーセント再現するものではない。

ユニオン型

最もシンプルな代替案はユニオン型を用いる方法。

type YesNo = "yes" | "no";
 
function toJapanese(yesno: YesNo) {
  switch (yesno) {
    case "yes":
      return "はい";
    case "no":
      return "いいえ";
  }
}

ユニオン型とシンボルを組み合わせる方法もある。

const yes = Symbol();
const no = Symbol();
type YesNo = typeof yes | typeof no;
 
function toJapanese(yesno: YesNo) {
  switch (yesno) {
    case yes:
      return "はい";
    case no:
      return "いいえ";
  }
}

オブジェクトリテラル

オブジェクトリテラルを使う方法もある。

const Position = {
  Top: 0,
  Right: 1,
  Bottom: 2,
  Left: 3,
} as const; // 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 Position = typeof Position[keyof typeof Position];

この部分では、Positionオブジェクトの全てのプロパティの値から成るリテラル型の型エイリアス(Type Alias)を定義している。

  1. typeof Position: Positionオブジェクトの型を取得。
  2. keyof typeof Position: Positionオブジェクトの全てのキー(Top, Right, Bottom, Left)をユニオン型で取得します("Top" | "Right" | "Bottom" | "Left")。
  3. typeof Position[keyof typeof Position]: Positionオブジェクトの全てのプロパティの値(0, 1, 2, 3)をリテラル型のユニオン型で取得します (0 | 1 | 2 | 3)。
    最終的にPosition型エイリアスは、 0 | 1 | 2 | 3のユニオン型になります。
    これにより、Position型の変数は 0, 1, 2, 3 のいずれかの値しか持つことができません。

keyofの説明

keyofは、オブジェクト型を受け取り、そのオブジェクトのプロパティ名全てを文字列リテラル型のユニオン型として返す。
r
例えば、次のようなオブジェクト型 Person があるとする。

type Person = {
  name: string;
  age: number;
};

keyofを使用すると、Personオブジェクトのすべてのプロパティ名を取得できる。

type PersonKeys = keyof Person; // "name" | "age"
リテラル型

TypeScriptで特定の値そのものを表す型。
リテラル型では指定された値だけが使用でき、他の値は代入できない。

リテラル型は主に以下の3種類がある。

  1. 文字列リテラル型(String Literal Type): 文字列リテラル型は、特定の文字列値そのものを表す。
type Greeting = 'hello' | 'hi';
const greet: Greeting = 'hello'; // OK
const greet2: Greeting = 'hey'; // エラー: 型 '"hey"' の引数を型 'Greeting' のパラメーターに割り当てることはできません。
```

2. 数値リテラル型(Number Literal Type): 数値リテラル型は、特定の数値そのものを表す。

```typescript
type Age = 18 | 19 | 20;
const myAge: Age = 19; // OK
const yourAge: Age = 25; // エラー: 型 '25' の引数を型 'Age' のパラメーターに割り当てることはできません。
```

3. ブールリテラル型(Boolean Literal Type): ブールリテラル型は、true と false の2つのブール値そのものを表す。

```typescript
type SwitchStatus = true | false;
const isOn: SwitchStatus = true; // OK
```
リテラル型は、 | (パイプ) 演算子を使って組み合わせることができ、ユニオン型(Union Type)を作成できる。


引用:[列挙型(enum)の問題点と代替手段](https://typescriptbook.jp/reference/values-types-variables/enum/enum-problems-and-alternatives-to-enums)
asami-okinaasami-okina

ユニオン型

TypeScriptのユニオン型は「いずれかの値」を表現するもの。

ユニオン型の型注釈

ユニオン型の型注釈は、2つ以上のパイプ記号で繋げて書く。
例えば、数値型もしくはundefined型を表す場合は、number | undefinedのように書く。

let numberOrUndefined: number | undefined;

|は型のリストの冒頭におくこともできる。
型ごとに改行するときに、列が揃うため便利。

type ErrorCode =
  | 400
  | 401
  | 402
  | 403
  | 404
  | 405;

配列要素にユニオン型を使う際の書き方

配列の要素としてユニオン型を用いる場合は書き方に注意が必要。
例えばstringまたはnumberからなる配列の型を宣言することを考えよう。

stringまたはnumberをユニオン型で表現するとstring | numberになる。
配列型は要素の型に[]をつけて表現する。

これをそのまま繋げて考えると、次のような型を思いつくかもしれないがこれは間違い。

type List = string | number[];

これはstring型またはnumber[]型であることになっているため。
正しくは以下。特に配列をT[]形式で書いているときは()が必要になるので注意してください。

type List = (string | number) [];

ユニオン型と絞り込み

ユニオン型string | nullがstringなのかnullなのかを判定したいときは、TypeScriptの絞り込み(narrowing)を用いる。

絞り込みをするにはいくつかの方法があるが、代表例はif文。
条件分岐で変数が文字列型かどうかをチェックすると、同じ変数でも分岐内ではstring | null型がstring型だと判定される。

const maybeUserId: string | null = localStorage.getItem("userId");
 
const userId: string = maybeUserId; // nullかもしれないので、代入できない。
 
if (typeof maybeUserId === "string") {
  const userId: string = maybeUserId; // この分岐内では文字列型に絞り込まれるため、代入できる。
}

引用:[ユニオン型](https://typescriptbook.jp/reference/values-types-variables/union)
asami-okinaasami-okina

判別可能なユニオン型

TypeScriptの判別可能なユニオン型は、ユニオンに属する各オブジェクトの型を区別するための「しるし」がついた特別なユニオン型。

オブジェクト型からなるユニオン型を絞り込む際に分岐ロジックが複雑になる場合は判別可能なユニオン型を使うとコードの可読性と保守性がよくなる。

通常のユニオン型は絞り込みが複雑になる

TypeScriptのユニオン型は自由度が高く、好きな型を組み合わせられる。

次のUploadStatusはファイルアップロードの状況を表現したユニオン型です。アップロード中InProgress、アップロード成功Success、アップロード失敗Failureの組み合わせ。

type UploadStatus = InProgress | Success | Failure;
type InProgress = { done: boolean; progress: number };
type Success = { done: boolean };
type Failure = { done: boolean; error: Error };

UploadStatusの各状態を整理したのが次の表である。


状態を表示する関数を実装する。

function printStatus(status: UploadStatus) {
  if (status.done === false) {
    console.log(`アップロード中:${status.progress}%`); // Error
// Property 'progress' does not exist on type 'UploadStatus'.
// Property 'progress' does not exist on type 'Success'.
  }
}

この実装は、doneがfalseであることをチェックしている。
不具合はないはず。
しかし、コンパイラーにはprogressが無いと警告される。
これは、if分岐内でも、statusがSuccessやFailureかもしれないとコンパイラーが考えるため。

このエラーを解消するには、progressがあることをチェックする必要がある。
そうすると、コンパイラーはif分岐内のstatusはInProgressだと判断する。

function printStatus(status: UploadStatus) {
  if (status.done === false && "progress" in status) {
    //                         ^^^^^^^^^^^^^^^^^^^^追加
    console.log(`アップロード中:${status.progress}%`);
    // コンパイルエラーが解消!
  }
}

コンパイルエラーを起こさないように、すべての状態に対応した関数が次です

function printStatus(status: UploadStatus) {
  if (status.done) {
    if ("error" in status) {
      console.log(`アップロード失敗:${status.error.message}`);
    } else {
      console.log("アップロード成功");
    }
  } else if ("progress" in status) {
    console.log(`アップロード中:${status.progress}%`);
  }
}

あまり読みやすいとは言えないかもしれない。
こうしたオブジェクトのユニオン型は、判別可能なユニオン型に書き直すとよい。

判別可能なユニオン型とは?

TypeScriptの判別可能なユニオン型はユニオン型の応用。
判別可能なユニオン型はタグ付きユニオンや直和型と呼ぶこともある。

判別可能なユニオン型は次の特徴を持ったユニオン型。

  1. オブジェクト型で構成されたユニオン型
  2. 各オブジェクト型を判別するためのプロパティ(しるし:ディスクリミネータ)を持つ
  3. ディスクリミネータの型はリテラル型などであること
  4. ディスクリミネータさえあれば、各オブジェクト型は固有のプロパティを持っても良い

例えば、上のUploadStatusを判別可能なユニオン型に書き直すと次のようになる。

type UploadStatus = InProgress | Success | Failure;
type InProgress = { type: "InProgress"; progress: number };
type Success = { type: "Success" };
type Failure = { type: "Failure"; error: Error };


変わった点といえば、done: booleanがなくなり、typeというディスクリミネータが追加されたところ。

typeの型がstringではなく、InProgressなどのリテラル型になったことも重要な変更点。

判別可能なユニオン型の絞り込み

判別可能なユニオン型は、ディスクリミネータを分岐すると型が絞り込まれる。

function printStatus(status: UploadStatus) {
  if (status.type === "InProgress") {
    console.log(`アップロード中:${status.progress}%`);
                             
(parameter) status: InProgress
  } else if (status.type === "Success") {
    console.log("アップロード成功", status);
                              
(parameter) status: Success
  } else if (status.type === "Failure") {
    console.log(`アップロード失敗:${status.error.message}`);
                              
(parameter) status: Failure
  } else {
    console.log("不正なステータス: ", status);
  }
}

判別可能なユニオン型を使ったほうが、コンパイラーが型の絞り込みを理解できます。その結果、分岐処理が読みやすく、保守性も高くなる。

ディスクリミネータに使える型

ディスクリミネータに使える型は、リテラル型とnull、undefined。

  • リテラル型
    • 文字列リテラル型: (例)"success"、"OK"など
    • 数値リテラル型: (例)1、200など
    • 論理値リテラル型: trueまたはfalse
  • null
  • undefined

上のUploadStatusでは、文字列リテラル型をディスクリミネータに使った。
リテラル型には数値と論理値もある。
これらもディスクリミネータに使える。

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);
  }
}

type OkOrNotOk = 
  | { isOK: true; value: string } 
  | { isOK: false; error: string };
 
function handleStatus(x: OkOrNotOk) {
  if (x.isOK) {
    console.log(x.value);
  } else {
    console.log(x.error);
  }
}

nullと非nullの関係にある型もディスクリミネータになれる。
次の例では、errorプロパティがnullまたはErrorで、null・非nullの関係が成り立っている。

type Result = 
  | { error: null; value: string } 
  | { error: Error };
 
function handleResult(result: Result) {
  if (result.error === null) {
    console.log(result.value);
  } else {
    console.log(result.error);
  }
}

同様にundefinedもundefined・非undefinedの関係が成り立つプロパティは、ディスクリミネータになる。

type Result = 
  | { error: undefined; value: string } 
  | { error: Error };
 
function handleResult(result: Result) {
  if (result.error) {
    console.log(result.error);
  } else {
    console.log(result.value);
  }
}

ディスクリミネータを変数に代入する場合

ディスクリミネータを変数に代入し、その変数を条件分岐に使った場合も、型の絞り込みができる。

type Shape =
  | { type: "circle"; color: string; radius: number }
  | { type: "square"; color: string; size: number };

function toCss(shape: Shape) {
    const { type, color } = shape; // ディスクリミネータ
    switch (type) {
        case "circle":
            return {
                background: color,
                borderRadius: shape.radius,
            };
        case "square":
            return {
                background: color,
                width: shape.size,
                height: shape.size,
            };
    }
}


引用:[判別可能なユニオン型](https://typescriptbook.jp/reference/values-types-variables/discriminated-union)
asami-okinaasami-okina

インターセクション型

考え方はユニオン型と相対するもの。
ユニオン型がどれかを意味するならインターセクション型はどれも

言い換えるとオブジェクトの定義を合成させることを意味する。
インターセクション型を作るためには合成したいオブジェクト同士を&で列挙する。

type TwoDimensionalPoint = {
  x: number;
  y: number;
};
 
type Z = {
  z: number;
};
 
type ThreeDimensionalPoint = TwoDimensionalPoint & Z;
 
const p: ThreeDimensionalPoint = {
  x: 0,
  y: 1,
  z: 2,
};

xy平面上の点を表すTwoDimensionalPointを拡張してxyz平面上の点のThreeDimensionalPointに変換した。

プリミティブ型のインターセクション型

プリミティブ型のインターセクション型をつくることもできますが、作るとneverという型ができる。

type Never = string & number;
 
const n: Never = "2"; // Error

このnever型にはいかなる値も代入できない。
使い道がまるでないように見えますが意外なところで役に立つ。

インターセクション型を使いこなす

システムの巨大化に伴い、受け付けたいパラメーターが巨大化したとする。

type Parameter = {
  id: string;
  index?: number;
  active: boolean;
  balance: number;
  photo?: string;
  age?: number;
  surname: string;
  givenName: string;
  company?: string;
  email: string;
  phoneNumber?: string;
  address?: string;
  // ...
};

一見してどのプロパティが必須で、どのプロパティが選択可かが非常にわかりづらい。

これをインターセクション型とユーティリティ型のRequired<T>(全プロパティを必須にする)とPartial<T>(全プロパティをオプショナルにする)を使いわかりやすく表記できる。

必須とそうでないパラメータのタイプエイリアスに分類する

type Mandatory = {
  id: string;
  active: boolean;
  balance: number;
  surname: string;
  givenName: string;
  email: string;
};
 
type Optional = {
  index: number;
  photo: string;
  age: number;
  company: string;
  phoneNumber: string;
  address: string;
};

Required<T>, Partial<T>をつける

MandatoryはRequired<T>を、OptionalはPartial<T>をつける。

type Mandatory = Required<{
  id: string;
  active: boolean;
  balance: number;
  surname: string;
  givenName: string;
  email: string;
}>;
 
type Optional = Partial<{
  index: number;
  photo: string;
  age: number;
  company: string;
  phoneNumber: string;
  address: string;
}>;

インターセクション型で合成する

これで最初に定義したParameterと同じタイプエイリアスができた。

type Parameter = Mandatory & Optional;

引用:インターセクション型

asami-okinaasami-okina

型エイリアス

TypeScriptでは、型に名前をつけられる。
名前のついた型を型エイリアスと呼ぶ。

型エイリアスの宣言

型エイリアスを宣言するにはtypeキーワードを使う。
次の例はstring|number型にStringOrNumberという型名を名付けたもの。

type StringOrNumber = string | number;

型エイリアスは、stringなどのビルトインの型と同様に、変数や引数、戻り値の型注釈などで使える。

const value: StringOrNumber = 123;

型エイリアスの使用例

型エイリアスはさまざまな型に名前をつけられる。

// プリミティブ型
type Str = string;
// リテラル型
type OK = 200;
// 配列型
type Numbers = number[];
// オブジェクト型
type UserObject = { id: number; name: string };
// ユニオン型
type NumberOrNull = number | null;
// 関数型
type CallbackFunction = (value: string) => boolean;

引用:型エイリアス