Open17

『プロを目指す人のためのTypeScript入門』の読書メモ

おたきおたき

はじめに

フロントエンド開発では必須のTypeScriptという言語をちゃんと学びたいと思ったのがきっかけ。書籍「プロを目指す人のためのTypeScript入門」を以前買ったが、そのまま放置していた。学びをスクラップに残すことで最後まで読み切ろうと思う。
https://gihyo.jp/book/2022/978-4-297-12747-3

具体的な進め方

  • 1日1回スレッドを立てる(タイトルはYYYY-MM-DD)
  • 下記内容を記載する
    • 進めたページ
    • 学んだこと
  • 読書にかける時間は最大でも90分程度とする(仮)
    • 制限をかけたほうが集中できそうなので設定したが、キリの良いところまでやりたいこともありそうなので、徹底はしない。
おたきおたき

2024-03-30

進めたページ

  • 1章 イントロダクション(p1~p24)完了

学んだこと

  • 静的型付けのメリット2点

    • 型安全性
      • 間違ったプログラムを、コンパイラが型チェックにより検出してくれる仕組み
      • コンパイルエラーは、構文エラーと型エラー(型チェック失敗時に発生)の2種類ある
      • コンパイルエラーが存在することでミスの発見可能性が高くなり、バグ発生が少なくなるため、成果物のクオリティが上がる。
      • 名言だ..

        コンパイルエラーは普通
        コンパイルエラーはありがとう
        コンパイルエラーが出たらありがとう

    • ドキュメント化と入力補完
      • 型情報がソース内に書かれているため、大規模開発で他人が書いたコードを読解する手助けになる。
      • 型情報は、IDEやテキストエディタ上での入力補完にも役立つ
  • TypeScriptコンパイラの役割2点

    • 型チェック
      • 実際にプログラムを実行しなくても行える、静的なチェックのこと
      • ランタイムの挙動が型情報に依存しない、という原則がある
        • TypeScriptが活きるのはあくまでコンパイル時のため、プログラム実行時(ランタイム)ではない。
        • TypeScriptは、型の役割を型チェックに絞ってランタイムの挙動に手を出さないことで、JavaScriptの自然な拡張という立ち位置を保っている。
    • トランスパイル
      • TypeScriptのコードをJavaScriptのコードへ変換すること。コンパイルとも呼ばれる
      • トランスパイルは、大きく2つのステップからなる
        1. 型注釈を取り除くステップ (取り除く以外のことはしない)
        2. 新しい構文を古い構文に変換するステップ
      • TypeScript以外にも、Babel、esbuild、SWCなどのトランスパイル手段がある
        • これらは型注釈を取り除く機能を備えているため、TypeScriptコンパイラをすべて代替可能である。→ TypeScriptコンパイラは型チェックのみの用途として使い、トランスパイラはBabelを使うといったセットアップもよく見られるとのこと。
  • TypeScriptの環境構築と簡単な動作確認

    • typescriptと@types/nodeのインストール
    • npx tsc --inittsconfig.jsonを作成&いろいろ設定
    • npx tscでJSファイルにコンパイル
    • node.js上で実行
おたきおたき

2024/03/31

進めたページ

  • 2章 基本的な文法(p25~34)
    • 2.1 文と式
    • 2.2 変数の宣言と使用

学んだこと

  • 変数宣言はconst 変数名 = 式;の形式を取っており、これ自体は
  • 文と式の決定的な違いは、結果があるかどうか
    • 式・・・結果がある、一般的に何らかの計算を行うもの
    • 文・・・結果がない、プログラムの構造を指定して計算を組み立てるもの。
      • 下記の場合、if文は3行全体が1つの文である。また中にあるconsole.log(~);も文である
      if (i < 10) {
            console.log("iは10未満です")
      }
      
  • 式文は、式のあとにセミコロンを書く文のこと
    • console.log(hoge + fuga);においてはconsole.log(hoge + fuga)は式にあたる
    • 式を実行したいが結果はいらない場合に有用である
    • 関数呼び出し後、;で終わらせるのは、式文を書いているということを覚えておく
  • 識別子
    • 変数宣言における変数名として使えるルールを満たした名前のこと
    • どの文字が利用可能かは、Unicodeという文字コードの規格で定義されている
    • 基本的に記号は対象外だが、$_は例外的にOK
  • 型注釈
    • 変数宣言の構文は、const 変数: 型 = 式となり、:型の部分が型注釈を指す
  • letは、宣言時に値を代入しなくてもよい。型も付与できる
    • if文などが絡むロジックで見ることがあるかも?
    let hoge: string, fuga: string;
    hoge = "おはよう";
    fuga = "世界!";
    
  • 型注釈が書かないとコンパイルエラーにならないのは、変数にundefinedが入る可能性があると、推論が働くからである。
// コンパイルエラーにならない
let hoge, fuga
hoge = "やほー,";
console.log(hoge + fuga); // fugaはundefinedのため

// コンパイルエラーになる
let hoge: string, fuga: string
hoge = "やほー,";
console.log(hoge + fuga); // fugaはstringが想定されているが、代入されずに使われているため
  • コラム: letを避けてプログラムを読む人の負担を減らそう
    • 極力const を使って変数を宣言すべき
    • ほとんどの変数は1度代入すれば十分、再代入の必要がない。
    • あえてletを使うことは、「この変数はあとで再代入されますよ」という意思表示になるため、プログラムを読む人がそこに意識を割かなければならなくなる。
おたきおたき

2024/04/02

進めたページ

  • 2章 基本的な文法(p34~)
    • 2.3 プリミティブ型

学び

  • 値はプリミティブとオブジェクトに大別される

  • TypeScriptにおいて、number型では整数と少数の区別がない

  • リテラル

    • なんらかの値を生み出すための式である
    • 数値リテラルは、数値を生成するための式。5という数値リテラルの計算結果は5という意味。
  • テンプレートリテラル

    • 通常の文字列リテラルとは違い、改行を使うことができる

      const message: string = `Hello
      world!`;
      console.log(message);
      
    • ${式}という構文を用いれば、式を文字列に埋め込むことができる。式の中身は文字列以外でも良い。

      const str1: string = "hoge";
      const num1: number = 3;
      console.log(`${str1}, ${num1}`); // "hoge, 3"
      
    • エスケープシーケンス

      • 改行文字などを表現したいときに有用。テンプレートリテラルでも実現可能だが見づらい。
      • \nでエスケープさせて使う
        console.log("Hello, \nworld");
        
  • 真偽値と真偽値リテラル

    • 真偽値はtruefalseの2種類の値からなるプリミティブ
    • リテラルについてもtrueとfalseのみ。
  • null と undefined

    • 各プリミティブに所属する値は、それぞれ1種類でnullundefinedのみ。
    • どちらも「データがない」状況を表すのに有用
    • TypeScriptの言語仕様上undefinedのほうがサポートが厚いため、筆者としてはundefiendを使うことを推奨。
  • 明示的な型変換

    • 数値への型変換の際は、NaNに気をつける
      const num1 = Number(true);
      console.log(num1); // 1
      
      const num2 = Number(false);
      console.log(num2); // 0
      
      const num3 = Number(null);
      console.log(num3); // 0
      
      const num4 = Number(undefined);
      console.log(num4); // NaN
      
  • 真偽値への型変換の規則

真偽値への変換結果
数値 0とNaNがfalse、ほかはすべてtrue
文字列 空文字("")のみfalse、他はすべてtrue
null・undefined false
オブジェクト すべてtrue
おたきおたき

2024/04/03

進めたページ

  • 2章 基本的な文法(p47~54)
    • 2.4 演算子

学んだこと

  • 演算子
    • 式を作るために用いられる記号
    • 演算子の構成要素になっている式は、オペランドと呼ばれる
    • x * y + 1 → *演算子は、x * yという式を生成し、+演算子が x * y と 1 という式をつなげる
  • 算術演算子
    • 二項演算子(6種類)
      • +, -, *, /, %, **
      • オペランドが2つある演算子のこと
      • 式1 + 式2 のような場面で用いる
    • 単項演算子(4種類)
      • +, -, ++, --
      • オペランドが1つの演算子
      • -式のように1つの式に付与して、新たな式を構成する
    • 数値以外の式を算術演算子のオペランドとして使用すると、コンパイルエラーになる
      • ただし+は文字列連結にも使うため例外である
    const str: string = "123";
    console.log(123 * str);
    // TS2363: The right-hand side of an arithmetic operation must be of type 'any', 'number', 'bigint' or an enum type.
    
    • +演算子は数値に変換する手法の一つ。Number(str)のほうがわかりやすいため、筆者はこちらを推奨していた。
    // パターン1
    const str1: string = "123";
    console.log(+str1 * 100); // 12300
    
    // パターン2
    const str2: string = "123";
    console.log(Number(str2) * 100): // 12300
    
    • インクリメント・デクリメント演算子
      • 副作用として使われることが多い。(副作用は返り値を返す以外で発生する影響のこと)
      • 基本は式文として使われ、計算結果(返り値)を無視している
        // 式文として使う例
        let foo = 10;
        foo++;
        console.log(foo); // 11
        --foo;
        console.log(foo); // 10
        
      • 返り値としての機能もある(ただし可読性下がる)
        • ++--を後ろにつけると「変動前の変数の中身」が、前につけると「変動後の変数の中身」が返される。
        // 返りとして使う例
        let foo = 10;
        console.log(++foo); // 変動後の11
        console.log(foo--); // 変動前の11
        
  • 等価演算子
    • ==!=は基本的に使わないと覚えておけば間違いない
    • ただし==を使用しても良い場面が1つだけある。それがx == nullの比較をする場合。
      • null と undefinedはどちらもデータがないことを表すため、両者を一緒くたに扱うときに便利。
      • x === null || x === undefinedと書くこともできるが、x == nullのほうがシンプル
  • NaNには注意せよ
    • 比較演算子や等価演算子を数値に対して用いる場合、どちらかのオペランドがNaNだと、常にfalseが返される
    • 例えば、xがNaNのときx < 100, x === 100, X >100のいずれもfalseとなる。x === NaNですらfalseとなる
    • NaNか判定するには、Number.isNaN(x)という関数を用いれば可能。
おたきおたき

2024/04/05

進めたページ

  • 2章 基本的な文法(p54~61)
    • 2.4 演算子: 完了

学んだこと

  • 論理演算子

    • &&, ||, !, ??などがある
  • !はオペランドを真偽値に変更した結果をさらに反転させるという挙動をもつ。

    • 例えば、123を真偽値に変換させるとtrueなので!123はfalse。
    • この挙動を応用して、下記のように!!式とすることもできる(※ただし賛否両論あり)
      • !!式は、!(!式)という意味なので真偽値反転させた結果に対して、さらに反転させる
      • だが、値の真偽値変換はBoolean(式)のほうが標準的である。
    const input1 = "123";
    const input2 = "";
    
    // !!で真偽値を2回反転
    const input1isNotEmpty = !!input1; // true
    const input2isNotEmpty = !!input2; // false
    
  • ??演算子と||の使い分け

    • x ?? y という形で使用する2項演算子
    • xが null または undefinedのときyを返し、それ以外はxを返す
    • https://zenn.dev/link/comments/0d469f25b28356 でも述べたが、nullとundefinedは「データがない」ことを表すの特化しているため、「データが無い場合は代替の値を使う」ケースに適する。
      • 動きは||と似ているが空文字、0、falseなどの値もないものとして扱うため、「空文字の場合、代替でデータを使う」ケースの場合は||を使う。
    // 環境変数SECRETが存在しない場合、左辺はundefinedとなり、右辺が返される
    cosnt secret = process.env.SECRET ?? "デフォルト値が入ります";
    console.log(`secretは${secret}です。`)
    
  • 条件演算子

    • TypeScriptにおいて3つのオペランドを持つ唯一の演算子。三項演算子とも呼ばれる
    • 使い方は、条件式 ? 真の場合の式 : 偽の場合の式
      • 条件式の部分においては、真偽値型以外でもOK
      • 例えば、num ? x : yという式でnumが0(またはNaN)のときはyが返される。
    • 条件演算子の返り値の型は、真の場合と偽の場合の型によって決まる
  • 論理代入演算子の特殊な挙動

    • ES2021で、&&-, ||=, ??=といった演算子が登場した
    • 変数 ||= 式の場合、必要でなければ変数の再代入が行われない(=が評価されない)特徴あり。
    • 変数 ||= 式と同一の挙動になるのは、変数 || (変数 = 式)である
    let userName = "otaki";
    // userNameがtrueなら、再代入が行われない
    userName ||= getDefaultName();
    console.log(userName); 
    
おたきおたき

2024/04/08

進めたページ

  • 2章 基本的な文法(p62~72)
    • 2.5 基本的な制御構文: 完了
    • 2.6 力試し:完了

学んだこと

  • 条件分岐(if文やswitch文) やループ(while文、for文)の使い方の基本を学んだ
  • if文
    • {} はブロックという構文であり、1つの文として扱われる
    • そのため if (条件式) 文 というifの基本構文において、文の位置に {} を持ってきても問題ない。
  • switch文
    • ifとは違い switch (式) { ... } における、{} はブロックではなく構文の一部。
    • case節は、break;で終わらせるのが原則。ただし、なくてもTypeScript的には問題ない
    • break文の書き忘れを防止するコンパイラオプションに、noFallthroughCaseInSwitch というものがあるので、有効化してもよい。
おたきおたき

2024/04/09

進めたページ

  • 3章 オブジェクトの基本とオブジェクトの型(p73~84)
    • 3.1 オブジェクトとは: 完了

学んだこと

  • TypeScriptのオブジェクトは、連想配列である
  • オブジェクトリテラル
    • const obj = {}{ ... }の部分を指す

    • プロパティの定義

      • プロパティ名: 式の構文
      • 下記のように、条件演算子(=式)を使うことも可能
      const user = {
          name: input ? input : "名無し", 
          age: 20,
      };
      
      • プロパティ名: 変数名という形において、プロパティ名と変数名が同じ場合は省略可能。
      const name = input ? input : "名無し";
      // 省略しないパターン
      const user = {
          name: name
          age: 20,
      };
      // 省略パターン
      const user = {
          name, // プロパティ名のみになる
          age: 20,
      };
      
      • オブジェクトリテラルのプロパティ名には、文字列リテラル・数値リテラルを使用できる。
      const obj = {
          // 文字列リテラルの例
          "hoge": 123,
          "hoge fuga": -500,
          // 数値リテラルの例
          1: "one",
          2.05: "two point o five",
      };
      
      console.log(obj.hoge);
      console.log(obj["hoge fuga"]);
      console.log(obj[1]);
      console.log(obj[2.05]);
      
      • プロパティ名を動的に計算させることも可能
      • [式]という構文を使い、式の評価結果となる文字列をプロパティ名とする
      const propName = "hoge";
      const obj = {
          [propName]: 123,
      };
      console.log(obj.hoge);
      
    • オブジェクトはいつ同じになるか?

      • 明示的にコピーしなければ同じである
    • スプレッド構文でオブジェクトをコピーする場合、ネストしたオブジェクトには注意

      • スプレッド記法は各プロパティが同じ値を持つ新しいオブジェクトを作るためのものであるため、プロパティ内のオブジェクトについては、変わらずコピーされない
const foo = { obj: { num: 1234 } };
const bar = { ...foo };
bar.obj.num = 0;

console.log(foo.obj.num); // 0
console.log(foo.obj === bar.obj); // true(プロパティ内のオブジェクトは同じものである)
おたきおたき

2024/04/13

進めたページ

  • 3章 オブジェクトの基本とオブジェクトの型(p85~95)
    • 3.2 オブジェクトの型: 完了

学んだこと

  • type文
    • type 型名 = 型;の構文
    • 新たに型を作って利用可能にするものではなく、すでにある型に別名をつけるだけのもの
type HogeFugaObj = {
  hoge: number;
  fuga: string;
};
const obj: HogeFugaObj = {
  hoge: 123,
  fuga: "hello",
};
  • interface宣言
    • interface 型名 = オブジェクト型の構文
    • type文とは違い、扱えるのはオブジェクト型のみ
    • 殆どの場合、type文に置き換えられるため、筆者はinterfaceを使わない流儀があるとのこと。
      • 特定の場合(Declaration Merging)に限り、必要である
    • 2014年以前は、型宣言がInterfaceのみ利用可能という歴史的経緯がある
interface HogeFugaObj {
  hoge: number;
  fuga: string;
}

const obj: HogeFugaObj = {
  hoge: 123,
  fuga: "hello",
};
  • インデックスシグネチャ
    • 任意のプロパティ名を許容する型のこと(オブジェクト型の中で使用できる)
    • オブジェクト型の中に[キー名: string]: 型;と書くのが基本形である。
      これは任意の名前のプロパティが型をもつという意味をもつ。
// インデックスシグネチャにより、任意のプロパティ名がnumber型を持つと宣言
type PriceData = {
  [key: string]: number;
};

const data: PriceData = {
  apple: 220,
  coffee: 120,
  bento: 500,
};

data.chicken = 250; // 新たなプロパティに対しても代入可能
data.弁当 = "hoge"; // プロパティがnumber型のため、コンパイルエラー発生
  • インデックスシグネチャに潜む罠 P90~91のコラム学びが多い

    • プロパティ存在有無に関わらず、どんな名前のプロパティにもアクセスできるため、TypeScriptの型安全性が破壊される可能性がある。
    • 型安全性とは、プログラムが型注釈や型推論の結果に反した挙動をしないことを指す。TypeScriptコンパイラは、コンパイルエラーを発生させることで、コンパイルに成功したプログラムが型安全な挙動をすることを保証しようとする。
  • 型安全性が破壊される例①

    • MyObj型は任意のプロパティ名がnumber型をもつオブジェクトの型である
    • この性質により、obj.barundefinedにも関わらず、number型と推論されてしまう。
type MyObj = { [key: string]: number };
const obj: MyObj = { foo: 123 }; 
const num: number = obj.bar; // obj.barは、number型だと型推論されてしまうため
console.log(num); // undefinedが入ってしまう🤨
  • 型安全性が破壊される例②
    • 動的なプロパティ名の型がstringの場合、オブジェクトリテラルの型がインデックスシグネチャを持ってしまう。
    • 本来、objはfooプロパティしか持たないはずなのに、型推論の時点でpropNameが任意のstring型の値に変わってしまっている
const propName: string = "foo";
// objは、{[key]: string: number}型 に推論されてしまう🤨
const obj = {
  [propName]: 123,
};
console.log(obj.foo); // 123
  • 型安全性のためにはインデックスシグネチャの使用は回避すべき。多くの場合はMapオブジェクトで代替可能である


  • 読み取り専用プロパティの宣言
    • オブジェクト型のプロパティ名の前にreadOnlyを付与する
    • コンパイラが追加チェックを行うようになり、再代入時にコンパイルエラーが発生するようになる
type MyObj = {
  readonly hoge: number;
};

const obj: MyObj = { hoge: 123 };

obj.hoge = 456; // 再代入できない
  • typeof
    • typeof 変数名の形で変数がもつ型を取得できる
    • 型推論の結果を型として抽出・再利用したいときに、非常に効果的
  • typeofはいつ使うべきか
    • 基本的には、type文で明示的に宣言するほうがわかりやすいプログラムとなる
    • ただし、値が最上位の事実である場合は、typeofを使う場面として適している(以下例)
      • 値側commandListを変更すると、Command型が付随して変わるなど
const commandList = ["attack", "defend", "run"] as const;
// "atack" | "defend" | "run" 型
type Command = (typeof commandList)[number];

これを型が最上位の事実である書き方に直すと・・・文字列を2回書く手間になる🤨

type Command = "atack" | "defend" | "run";
const commandList: Command[] = ["atack", "defend", "run"];
おたきおたき

2024/04/15

進めたページ

  • 3章 オブジェクトの基本とオブジェクトの型(p95~103)
    • 3.3 部分型関係: 完了
    • 3.4 型引数を持つ型: 完了

学んだこと

  • 部分型とは2つの型の互換性を指す(型Sが型Tの部分型→S型の値がT型の値でもある)
    • 以下はFooBarBaz型FooBar型の部分型となる
type FooBar = {
  foo: string;
  bar: number;
};

type FooBarBaz = {
  foo: string;
  bar: number;
  baz: boolean;
};

const obj: FooBarBaz = {
  foo: "hoge",
  bar: 1,
  baz: false,
};

const obj2: FooBar = obj;
  • 部分型関係が発生する条件
    • オブジェクト型の場合、プロパティの包含関係によって発生する。
    • 以下2つの条件を満たせば、SがTの部分型になる
      1. TがもつプロパティがすべてSにも存在する
      2. 条件1の各プロパティについて、Sにおけるそのプロパティの型はTにおけるプロパティの型の部分型(または同じ型)である
        // HumanはAnimalの部分型
        type Animal = {
          age: number;
        };
        type Human = {
          age: number;
          name: string;
        };
        
  • 余剰プロパティに対する型エラーについて
    • オブジェクトリテラルに余計なプロパティが含まれると、エラーが発生することがある
    • このエラーは補助的なものであるため、常にチェックが行われるわけではない
    • 型注釈がある・オブジェクトリテラルを直接代入する、という2条件を満たす場合は追加の型チェックが行われる。

①型エラーが発生する場合

  • { name: string; age: number; telNumber: string }は、Userの部分型であることから、本来は型安全性は守られるはずだが、プログラマーのミス防止のためにチェックが行われている。
  • 型注釈がある変数にオブジェクトリテラルを直に代入する場合に限り発生する
type User = { name: string; age: number };
// error TS2353: Object literal may only specify known properties, and 'telNumber' does not exist in type 'User'.
const user: User = {
  name: "otaki",
  age: 26,
  telNumber: "1111111",
};

②型エラーが発生しない場合

  • { name: string; age: number; telNumber: string }がUserの部分型であり、型システム上問題ない。
  • userに代入されている式はただの変数のため、余剰プロパティのチェックは行われない。
type User = { name: string; age: number };
const obj = {
  name: "otaki",
  age: 26,
  telNumber: "1111111",
};
const user: User = obj;

  • 型引数をもつ型
    • 「型をつくるためのもの」であり、類似した構造をもつ型を扱いたい場合に有用である。
    • 具体的な型に言及せず、構造のみに言及しているため、1種の抽象化とも言える
    • Family<Parent, Child>という型を作っておけば、Family<string, number>Family<boolean, boolean>など応用を効かせられる。
type Family<Parent, Child> = {
  mother: Parent;
  father: Parent;
  child: Child;
};

const obj: Family<string, string> = {
  mother: "おかあ",
  father: "おとお",
  child: "べびー",
};
  • extends 型という構文で制約をかけることができる。
    • 下記例では、型引数は、常にHasNameの部分型でなければならないという意味
type HasName = {
  name: string;
};

// ParentとChildはHasNameの部分型である必要がある
type Family<Parent extends HasName, Child extends HasName> = {
  mother: Parent;
  father: Parent;
  child: Child;
};
  • オプショナルな型引数を付与することも可能。
    • ただし、オプショナルな型引数のあとにオプショナルでない型引数を宣言できない。後ろにまとめる必要あり。
type Animal = {
  name: string;
};

type Family<Parent = Animal, Child = Animal> = {
  mother: Parent;
  father: Parent;
  child: Child;
};

// 通常の使い方
type S = Family<string, string>;

// Family<Animal, Animal>
type T = Family;

// Family<string, Animal>
type U = Family<string>;
おたきおたき

2024/04/15

進めたページ

  • 3章 オブジェクトの基本とオブジェクトの型(p103~114)
    • 3.5 配列

学んだこと

  • 配列はオブジェクトの1種である

  • 配列の要素にアクセスすることをインデックスアクセスという

  • 配列は実際に0や1といった名前のプロパティを持ったオブジェクトである

    • 配列のプロパティにアクセスするときのインデックス名は、数値指定しないとコンパイルエラーになる
  • readonly配列型

    • readonly T[]という形式で書く。別の書き方でReadonlyArray<number>もある
    • 変更必要がないオブジェクトや配列の型は積極的にreadonlyを付与することで可読性が向上。
    • 配列を引数とする関数でも、読み取り専用の型とすることが推奨されている
const arr: readonly number[] = [1, 2, 3];
const arr2: ReadonlyArray<number> = [1, 2, 3];
  • readonlyを付与することで破壊的メソッド(push, unshiftなど)が使用できなくなる
const arr: readonly number[] = [1, 2, 3];
arr.push(222); // error TS2339: Property 'push' does not exist on type 'readonly number[]'.
  • for-of文

    • for (const 変数 of 式) 文が基本的な形。配列要素に対して順番に処理するのに適する
  • タプル型

    • 要素数が固定された配列型。
    • [ ]の中に型をコンマで並べて書く形式
let tuple: [string, number] = ["foo", 0];
tuple = ["hoge", 1];

const str = tuple[0]; // "hoge"
const num = tuple[1]; // 1
  • ラベル付きタプル型という機能もある。
    ただし、インデックスにアクセスしないと要素取得できない
type User = [name: string, age: number];
const otaki: User = ["otaki", 26];
console.log(otaki[1]); // otaki.ageみたいにはアクセスできない
  • タプル型とオブジェクト型は、異なる型の値を組み合わせて1つの値を作る点では類似している
    • [string, number]{ name: string, age: number }のようなイメージ
    • ただしプログラムのわかり易さでは、オブジェクト型が好まれる
    • ReactのuseStateは、あえてオブジェクト型よりもタプル型を選択している


  • readOnlyで読み取り専用タプル型を作ることもできる。要素を書き換え不可能にできる
let tuple: readonly [string, number] = ["foo", 0];
tuple[0] = "hoge"; // error TS2540: Cannot assign to '0' because it is a read-only property.
  • オプショナルな要素をもつタプル型も作ることが可能
// 型は [string, number, (string | undefined)?]
const test1: [string, number, string?] = ["hoge", 1, "fuga"];
const test2: [string, number, string?] = ["hoge", 1];
  • 配列要素にアクセスするときの危険性
    • 配列のインデックスアクセスにおいて、存在しない位置の要素に対してアクセスできてしまう🤨
    • TypeScriptは存在しないプロパティからはundefinedが得られるため、実際の型と食い違いが発生する
const arr = [1, 10, 100]; // 型上は、number[]のため要素数の情報は持ち合わせていない
console.log(arr[100]); // undefined
  • この危険性を回避するためにも、arr[100]のような インデックスアクセスは極力使用しないほうがよい。
    • 代わりにfor-of文などで使うのがよい
おたきおたき

2024/04/21

進めたページ

  • 3章 オブジェクトの基本とオブジェクトの型(p114~120)
    • 3.6 分割代入

学んだこと

  • 分割代入
    • 構文は、パターン = 式
    • { }の中に識別子をコンマ区切りで並べるパターンでは、以下の役割がある
      • 取得したいオブジェクトのプロパティ名を示す役割
      • それを格納する変数名を決める役割
    • プロパティ名と別の名前の変数を使うことも可能(下記例)
    • 宣言された変数には型注釈が付与できない
const obj = { foo: "ほげ", bar: "ふが", "foo bar": "いえっさー" };
const { foo, bar: fooVar, "foo bar": fooBar } = obj;

console.log(foo); // "ほげ"
console.log(fooVar); // ふが
console.log(fooBar); // いえっさー

  • 配列の分割代入では、空白を使って要素をスキップできる機能がある
    • 以下はarrの1,3,5番目の要素を取得する場合(0番目スタート)
  const arr = [1, 2, 4, 8, 16, 32];
  const [, foo, , bar, , baz] = arr;

  console.log(foo); // 2
  console.log(bar); // 8
  console.log(baz); // 32
  • 分割代入のデフォルト値について
    • デフォルト値はundefinedのみに対して適用される ことは覚える価値あり
type Obj = { foo?: number }; // number | undefined 型
const obj1: Obj = {};
const { foo = 500 } = obj1; // デフォルト値により、undefinedの可能性が排除される
console.log(foo); // 500

一方、null に対してはデフォルト値は機能しない

const obj = { foo: null };
const { foo = 123 } = obj;
console.log(foo); // null
  • オブジェクトのrestパターンについて
    • パターンとしては...変数名という構文であり、オブジェクトの一番最後で使用する
    • restパターンを複数ならべることはできない
    • 変数には、分割代入されたオブジェクトの残りのプロパティすべてをもつ新たなオブジェクトが代入される
    • 新しいオブジェクトが生成されるのを利用して、イミュータブルなオブジェクト操作に役立つ
const obj = { foo: "ふー", hoge: "ほげ", fuga: "ふが" };
const { foo, ...rest } = obj;

console.log(foo); // ふー
console.log(rest); // { hoge: 'ほげ', fuga: 'ふが' } 👈️ こんな感じ
  • 配列におけるrestパターンも同様
const arr = [1, 1, 2, 3, 5, 8, 15];
const [first, second, third, ...rest] = arr;
console.log(first); // 1
console.log(second); // 1
console.log(third); // 2
console.log(rest); // [3, 5, 8, 15] 👈️ こんな感じ
おたきおたき

2024/04/28

進めたページ

  • 3章 オブジェクトの基本とオブジェクトの型(p121~133)
    • 3.7 その他の組み込みオブジェクト
    • 3.8 力試し

学んだこと

  • Dateオブジェクト

    • 日時データを扱うときは、ISO 8601形式のフォーマットで扱うのが一般的
    • 数値からDataオブジェクトを得るにはnew Data(数値)とする。この表現の使用場面は多い。
    • Date.now()は、現在時刻を数値表現で得るメソッドであり、(new Date()).getTime()と同じ挙動。
    • Dateオブジェクトは使いにくいオブジェクト としても有名
      • 日時操作のやりにくさ、ミュータブルであること、タイムゾーン周りの扱い難しいなどなど。。。
      • Temporalという新しい組み込みオブジェクト郡がの導入が進んでいるため、今後はこっちが主流になるかも???
        https://tc39.es/proposal-temporal/docs/ja/index.html
  • 正規表現(Regular Expression)

    • ちょっと複雑なのでいったん目を通すだけにする。必要になった際に見よう
  • Mapオブジェクト

    • Mapは真の連想配列
      • Map<K, V>型のオブジェクトとなる
      • キーに対して値を保持する機能があり、このペアを好きなだけ保持できるオブジェクト。
      • ただのオブジェクトはプロパティ名が原則文字列だったが、Mapはプリミティブだけでなくオブジェクトもキーにできるのが特徴。
    • setメソッドでは、キーと値のそれぞれを引数に渡すことで、保存可能
    • getメソッドでは、キーを受け取り、対応するV型の値を返す。ただし値が存在しない場合、undefinedとなる

イメージ

const map: Map<string, number> = new Map();
const map = new Map<string, number>(); // この書き方でもOK
map.set("otaki", 26);

console.log(map.get("otaki")); // 26
console.log(map.get("otaki2")); // undefined
  • Setオブジェクト

    • Mapの簡易版のようなオブジェクトであり、集合を表すことができる
    • 型としてはSet<T>であるため、型引数は1つ受け取る
    • addメソッドでT型の値追加、deleteメソッドで値を取り除くことができる
  • その他に類似したものとして、WeekMapWeekSetがある

    • 特徴として、キーにオブジェクトしか使用できない、列挙系のメソッド(keys, values等)を持たない。
    • ガベージコレクトされるかもしれないオブジェクトをキーに用いる場合は、MapよりWeekMapの方がよい。
  • {}型の変数には、オブジェクトだけでなく数値や文字列などの値も代入できる。ただし、null, undefinedは例外になっている。nullとundefinedはプロパティアクセス時にランタイムエラーが発生する唯一の値であるため、オブジェクトの範疇にない。

  • 力試し

    • CSV形式のデータ構造をもった文字列からオブジェクトの配列を組み立てる練習
実際のコード
type User = {
  name: string;
  age: number;
  premiumUser: boolean;
};

const data: string = `
otaki,26,1
john Smith,17,0
Mary Sue,14,1
`;

const users: User[] = [];
const lines = data.split("\n"); // 改行コードで文字列分割
console.log(lines);
for (const line of lines) {
  // 空文字の場合はスキップ
  if (line === "") {
    continue;
  }
  const [name, ageString, premiumUserString] = line.split(","); // カンマで文字列分割
  const age = Number(ageString);
  const premiumUser = premiumUserString === "1";

  users.push({ name, age, premiumUser });
}
console.log(users);

for (const user of users) {
  if (user.premiumUser) {
    console.log(`${user.name} (${user.age})はプレミアムユーザーです。`);
  } else {
    console.log(
      `${user.name} (${user.age})はプレミアムユーザーではありません。`
    );
  }
}
おたきおたき

2024/04/29

進めたページ

  • 4章 TypeScriptの関数(p135~144)
    • 4.1 関数の作り方

学んだこと

  • 関数宣言は巻き上げ(hoisting)という、宣言前に関数が使えるという特殊な挙動を持つ
    • 関数はプログラム実行開始時からすでに存在している
  • 返り値がない関数はvoid型という特殊な型を使う
    • void型であっても早期リターンによる、関数の実行中断は可能である
  • returnと返り値の間に改行はいれてはいけない。改行を入れると、そこでセミコロンが補われる
  • 関数式は巻き上げ機能を持たないため、宣言されていない変数を使うことはできない。

関数式(通常)

type Human = {
  height: number;
  weight: number;
};

const calcBMI = function ({ height, weight }: Human): number {
  return weight / height ** 2;
};

const otaki: Human = { height: 1.84, weight: 88 };
console.log(calcBMI(otaki));

関数式(アロー関数)

type Human = {
  height: number;
  weight: number;
};

const calcBMI = ({ height, weight }: Human): number => {
  return weight / height ** 2;
};

const otaki: Human = { height: 1.84, weight: 88 };
  • function関数式にできてアロー関数式にできないこととして、コンストラクタを作ることがある。
    しかし、クラス機能がある現在においては、function関数式をあえて使う場面は少ない。

  • アロー関数の省略形の注意点

    • 返り値の式としてオブジェクトリテラルを使いたいときは、リテラルを( )で囲む必要がある。
    • 囲まないと=>の右の{ }が通常の省略形でないアロー関数の中身を囲む{ }とみなされてしまう。
type Human = {
  height: number;
  weight: number;
};

type ReturnObj = {
  bmi: number;
};

// 正しい書き方
const calcBMIObject = ({ height, weight }: Human): ReturnObj => ({
   bmi: weight / height ** 2,
});

// これはコンパイルエラー発生(返り値を返していないとみなされる)
const calcBMIObject = ({ height, weight }: Human): ReturnObj => {
  bmi: weight / height ** 2,
};
  • 関数のメソッド記法もある
    • この書き方はオブジェクトリテラルの中で使用できる、プロパティ定義記法の1つ。
const obj = {
  // メソッド記法
  double(num: number): number {
    return num * 2;
  },

  // 通常記法 + アロー関数
  double2: (num: number): number => num * 2,
};
おたきおたき

2024/04/30

進めたページ

  • 4章 TypeScriptの関数(p144~151)
    • 4.1 関数の作り方

学んだこと

  • 可変長引数の宣言
    • rest引数構文で実現できるため、書き方...引数名: 型となる
    • 引数リストの最後で1回だけ使用できる
    • rest引数の型は必ず配列型(またはタプル型)である必要がある
const sum = (...args: number[]): number => {
  let result = 0;
  for (const number of args) {
    result += number;
  }
  return result;
};

console.log(sum(1, 2, 3)); // 6
console.log(sum(1, 2, 3, 4, 5)); // 15
console.log(sum()); // 0

この書き方は、型の指定が適切でないためコンパイルエラーになる

const sum = (...args: number) => { ・・・};

rest引数を通常の引数と併用する場合、引数が一つも与えられないと、baseに相当する引数がないのでコンパイルエラーになる

const sum = (base: number, ...args: number[]): number => {
  let result = base * 1000;
  for (const num of args) {
    result += num;
  }
  return result;
};

console.log(sum(1, 10, 100)); // 1110
console.log(sum(123, 456)); // 123456
console.log(sum()); //  Expected at least 1 arguments, but got 0🤨
  • 関数呼び出しにおけるスプレッド構文
    • 呼び出し時に、...式という構文を使用することができる
    • スプレッド構文は、可変長引数と同時に使われることが多い。配列の要素数は何個あるか、一般的に不明であるため。
const sum = (...args: number[]): number => {
  let result = 0;
  for (const num of args) {
    result += num;
  }
  return result;
};

const nums = [1, 2, 3, 4, 5];  👈 型はnumber[]のため、要素数までは特定できない
console.log(sum(...nums)); // 15 👈️ スプレッド構文でsum(1,2,3,4,5)
  • nums(要素数不明の配列)をsum3(引数3つ受け取る関数)に渡そうとすると、コンパイルエラーになる。
    • number[]型であるnumsの要素は3つとは限らず、3つの引数は渡せないかもしれないから。
🤨 コンパイルエラー発生
const sum3 = (a: number, b: number, c: number) => a + b + c;
const nums = [1, 2, 3]; 
console.log(sum3(...nums));const sum3 = (a: number, b: number, c: number) => a + b + c;
const nums: [number, number, number] = [1, 2, 3]; // 👈️ タプル型でnumsに要素数を持った型を付与
console.log(sum3(...nums));
  • 基本的に固定長引数の関数に対して、スプレッド構文による関数呼び出しをする必要はあまりない。
  • 可変長引数の関数と組み合わせるのが主であるとおぼえておこう。
  • オプショナルな引数の宣言
    • 複数のオプショナル引数を指定することは可能
    • ただしオプショナル引数よりあとに通常の引数を宣言できない(以下例)
// 🤨 error TS1016: A required parameter cannot follow an optional parameter.
const toLowerOrUpper = (str?: string, upper: boolean): string => {};

rest引数とオプショナル引数は併用可能であり、オプショナル引数 → rest引数という順番になる

const test = (hoge: string, fuga?: number, ...rest: number[]) => {};
  • コールバック関数を引数として受け取る関数は高階関数と呼ばれる
    • 配列のmapメソッド,filterメソッドなどが該当する
おたきおたき

2024/05/01

進めたページ

  • 4章 TypeScriptの関数(p152~160)
    • 4.2 関数の型

学んだこと

  • 関数型

    • 通常は、(引数リスト) => 返り値の型という形式になる
    • 引数を受け取らず返り値がない関数 () => void
    • 0個以上の任意の数を受け取って数値を返す関数 (...args: number[]) => number
  • 関数型中の引数は型チェックに影響を与えない

    • 型が同じならば、引数名が異なっても同じとみなされる
      例:(hoge: number) => void(fuga: number) => void は同じ
    • あくまでコーディング支援の充実のため(VSCode上でやると型情報が補完される)
  • 関数の返り値の型注釈は省略すべきかどうか

    • 関数を作る理由は、関数を使う側が関数の説明と型だけ見れば中身を知らなくてもよい状態をつくること。そのため、返り値の型を知るために関数の中身をみるのは本末転倒である。
    • 型を明示する利点
      • 関数内部で返り値の型に対して、型チェックを働かせられる点。実装ミスを事前に防止できる。
      • 返り値の型注釈がある場合、ない場合でコンパイルエラーの発生位置が変わることがある(以下例)

正しい書き方

function range(min: number, max: number): number[] {
  const result = [];
  for (let i = 0; i < max; i++) {
    result.push(i);
  }
  return result;
}
const arr = range(5, 10);
for (const value of arr) console.log(value); // 5, 6, 7, 8 ,9, 10

関数rangeに型注釈つける + returnを書き忘れた場合

  • 関数に対するコンパイルエラーとして検出されるので原因が特定しやすく嬉しい👍
// ⛔error TS2355: A function whose declared type is neither 'undefined', 'void', nor 'any' must return a value.
function range(min: number, max: number): number[] {
  const result = [];
  for (let i = 0; i < max; i++) {
    result.push(i);
  }
}
const arr = range(5, 10);
for (const value of arr) console.log(value); // 5, 6, 7, 8 ,9, 10

関数rangeに型注釈つけない + return 忘れた場合

  • 関数に対するコンパイルエラーでないため、原因が特定しづらい🤨
function range(min: number, max: number) {
  const result = [];
  for (let i = min; i <= max; i++) {
    result.push(i);
  }
}
const arr = range(5, 10);
// ⛔ 型 void には、反復子を返す [Symbol.iterator]() メソッドが必要です。
for (const value of arr) console.log(value);
  • 返り値の型を明示しない場合は、返り値の型は型推論によって決められる

    • 関数の中身のコードが、その関数の返り値の型を決めている = 関数の中身全体が、型に関する真実の源になっている
  • 関数の返り値の型を明示するかどうかは、真実の源がどこにあるかを考えながら決めると良い。

    • 返り値の型を省略する ・・・ 型推論に任せることになるので、関数の中身が真実であるという判断
    • 返り値の型を明示する ・・・この関数はこの型の値を返すべきであるという真実を用意する判断
おたきおたき

2024/05/02

進めたページ

  • 4章 TypeScriptの関数(p161~168)
    • 4.2 関数の型

学んだこと

  • 返り値の型による部分型関係
    • SがTの部分型である
    • (引数リスト) => Sという関数型に対して、(引数リスト) => Tが部分型
      • このときS型の値を返す関数は、T型の値を返す関数の代わりに使える
        • ただし、引数の型が同じでなければこの理屈は通らないので注意。
      • 以下はHasNameAndAgeHasNameの部分型である例
// T型
type HasName = {
  name: string;
};

// S型
type HasNameAndAge = {
  name: string;
  age: number;
};


const fromAge = (age: number): HasNameAndAge => ({
  name: "Jonh Smith",
  age,
});

// T型の値を返す関数にS型を返す関数代入
const f: (age: number) => HasName = fromAge;
const obj: HasName = f(100);
  • 返り値の部分型に関して、void型は特殊な振る舞いをする
    • どんな型を返す関数型も、void型を返す関数型の部分型として扱われる(以下例)
const f = (name: string) => ({ name });
const g: (name: string) => void = f;
  • 引数の型による部分型関係(🤨ちょっとこれややこしい)
    • 型Sが型Tの部分型であるとき、Tを引数に受け取る関数の型はSを引数に受け取る関数の型の部分型
      • つまり、引数としてTの代わりにSを引数として受け取っても良い。
      • 引数の場合、部分型関係の向きが逆になっている点に注意。
    • 以下例では、HasNameを引数として受け取る関数 showMoreの型は、HasNameAndAgeを引数に受け取る関数 gの型の部分型となることを指している。
      • 関数gでは、引数にHasNameAndAge型で受け取るが、それはHasNameAge型として扱われる
// T型
type HasName = {
  name: string;
};

// S型(HasName型の部分型)
type HasNameAndAge = {
  name: string;
  age: number;
};

const showName = (obj: HasName) => {
  console.log(obj.name);
};

// T型を引数にとる関数をS型を引数にとる関数として扱うことができる
const g: (obj: HasNameAndAge) => void = showName;

g({ name: "otaki", age: 26 }); // otaki
  • 引数の数による部分型関係
    • ある関数型Fの引数リスト末尾に、新たな引数を追加した関数型Gを作った場合、FはGの部分型となる
    • 以下例では、UnaryFuncはBinaryFuncの部分型である
type UnaryFunc = (age: number) => number;
type BinaryFunc = (left: number, right: number) => number;

const double: UnaryFunc = (arg) => arg * 2;
const add: BinaryFunc = (left, right) => left + right;

// UnaryFuncをBinaryFuncとして使うことができる
const hoge: BinaryFunc = double;
console.log(hoge(10, 100)); // 20
  • 関数型FがGの部分型になる条件(引数の数、引数、返り値 を組み合わせた場合)
    • Fの引数の数がGの数以下
    • 両方に存在する引数について、反変の条件を満たす
    • 返り値については共変の条件を満たす