「手を動かしながら学ぶTypeScript」を写経しながら読んだ

ここしばらく仕事でTypeScriptも書いているものの、まだ雰囲気で書いている感がありました。

そのため、腰を据えて学ぼうと考え、手を動かしながら理解できるチュートリアル的な書籍を探したところ、「手を動かしながら学ぶTypeScript」に出会いました。

 
出版社のWebサイトを見ると

本書では「JavaScript 開発の経験はあるが、TypeScript についてはこれから学ぼうと思っている」という方を対象に、次のように本書の前半部分ではTypeScriptの基礎を解説し、それ以降では「実際に動くものを作ってみる」という内容となっています。

手を動かしながら学ぶ TypeScript | 書籍詳細|株式会社 C&R研究所

とあり、自分の目的と一致してそうでした。

そこで、写経しながら読んだ時のメモを残します。

 
なお、書籍ではwebpackを使っているものの、仕事ではViteを使っていることもあり、Vite + TypeScriptで写経しました。

また、最新のTypeScriptなどで写経しても問題ないだろう(し、問題あったら対応してみよう)と考えて、書籍とは異なるバージョンのTypeScriptで写経しました。

 
目次

 

環境

 

感想

書籍では、チュートリアル的に

  • Node.js
  • ブラウザで動くアプリ
  • ReactのUIライブラリ

の3別分野のアプリを作っていきます。

仕事でReactを使ったWebアプリケーションを書いていることから、特に詰まることもなく進められました。

Chapter3までが基本的な機能の解説に感じた一方で、4以降はTypeScript以外の知識も必要にも感じました。

そのため、WebアプリケーションやReactを書いていないとしたら、Chapter4や5は難しく感じるかもしれないです。

 
また、書籍の流れとしては、

  1. まずTypeScriptで書く
  2. 書いたコードの問題点を挙げる
  3. 問題点を解消するようなTypeScriptの機能を解説
  4. 解説したTypeScriptの機能を作って、1のコードをリファクタリング

が繰り返されるため、「TypeScriptの機能を使うと、こんなふうに書けるんだ」と理解しやすかったです。

著者のみなさま、良い本をありがとうございました。

 
なお、自分が雰囲気で書いていた

  • as を使った型アサーション
  • keyoftypeof の使い方
  • 高度な型の Mapped Types や Conditional Types

あたりは、書籍を写経するだけでは理解が浅そうだったので、自分の身近な内容を使い、TypeScript Playgroundを使って再実装してみました。
https://www.typescriptlang.org/play/

 

この書籍で手を動かせたため、次は評判の良い以下の本を読み進めようと思いました。

   
以降は個人的なメモとなります。

 

Viteでの環境構築

書籍とは異なりViteで環境構築をしたことから、作業ログを残しておきます。

Vite環境を作るため、Webサイトの手順に従って作業を進めました。
はじめに | Vite

 

Chapter4向け

webpackの場合は、追加で色々インストールする必要がありました。

一方Viteであれば、手元では npm create だけで十分でした。

$ npm create vite@latest
Need to install the following packages:
  create-vite@4.0.0
Ok to proceed? (y) y
✔ Project name: … browser-app
✔ Select a framework: › Vanilla
✔ Select a variant: › TypeScript

Scaffolding project in path/to/awesome-typescript-book/browser-app...

Done. Now run:

  cd browser_app
  npm install
  npm run dev

 

Chapter5向け

Chapter4と同様にセットアップしました。

$ npm create vite@latest
✔ Project name: … react-app
✔ Select a framework: › React
✔ Select a variant: › TypeScript

Scaffolding project in path/to/react-app...

Done. Now run:

  cd react-app
  npm install
  npm run dev

 
なお、 styled-components については別途インストールする必要があります。

$ npm install -D @types/styled-components

 
また、Viteで styled-components を使うには、以下のパッケージも追加で必要になりました。
How use with Vite.js, especially displayName for debugging. · Issue #350 · styled-components/babel-plugin-styled-components

$ npm install -D @babel/preset-typescript @babel/plugin-transform-typescript babel-plugin-styled-components

 
他に、Reactのバージョンを書籍の 17.0.2 ではなく 18.2.0 で写経したため、 React.VFC を使っていたところは React.FC へと差し替えました。
今更の React v18 : children の扱いが変わった (TypeScript) - かもメモ

 

書籍の内容を別の題材でも書いてみたところ

p93 型アサーション (type assertion)

アサーションについて改めて調べてみると、

TypeScriptが推論、分析された型は、任意の方法で上書きできます。これは、型アサーション(type assertion)と呼ばれるメカニズムによって行われます。TypeScriptの型アサーションは、純粋にコンパイラよりもその型をより良く理解していることだけでなく、後で推測するべきではないことをコンパイラに伝えています。

Type Assertion(型アサーション) - TypeScript Deep Dive 日本語版

とありました。

また、書籍 p94 に

アサーションの可否は、対象となる2つの型が包含関係にあるかどうかによって決定されます

とあり、「包含関係」が気になったので、別の題材で書いてみました。

まずはプリミティブ型の場合です。

// 一致
const a1 = 'シナノゴールド' as string

// 包含関係になっていない
const a2 = 'シナノゴールド' as number
// => Conversion of type 'string' to type 'number' may be a mistake 
//    because neither type sufficiently overlaps with the other. 
//    If this was intentional, convert the expression to 'unknown' first.

 
オブジェクト型の場合です。

// オブジェクト型
// 一致
const a3 = { name: 'シナノゴールド' } as { name: string }

// プロパティが異なる
const a4 = { name: 'シナノゴールド'} as { color: string }
// => Conversion of type '{ name: string; }' to type '{ color: string; }' may be a mistake
//    because neither type sufficiently overlaps with the other.
//    If this was intentional, convert the expression to 'unknown' first.
//    Property 'color' is missing in type '{ name: string; }' but required in type '{ color: string; }'.

// asで指定した型より実際のオブジェクトの方が、プロパティが多い
const a5 = { name: 'シナノゴールド', color: '黄' } as { name: string }

// asで指定した型より実際のオブジェクトの方が、プロパティが少ない
const a6 = { name: 'シナノゴールド' } as { name: string, color: string }

 
ただ、理由がなければ型アサーションを使わないほうが良さそうとも理解しました。

多くの場合、アサーションを使用すると、レガシーのコードを簡単に移行できます(また、コードベースにほかのコードのサンプルをコピー・ペーストしたりもできます)。しかし、アサーションの使用には注意が必要です。下記のように、必要なプロパティを実際に追加するのを忘れても、コンパイラはあなたを守りません

アサーションは害 | Type Assertion(型アサーション) - TypeScript Deep Dive 日本語版

 

p111 as constによる型アサーション

上記で見た as T ではなく as const とすることで、literal type wideningの動きを抑制するとありました。

literal type wideningのざっくりした説明は以下です。詳細は書籍のp53に載っています。

(53ページで説明した内容ですが、) 一言で言うと「ミュータブルな値の型は自動的に汎用型に変換される」というTypeScriptの仕様のことです

書籍「手を動かしながら学ぶTypeScript」 p111

 
literal type widening を実感するため、実際に試してみました。

const shinano_gold_name = 'シナノゴールド'
// => const shinano_gold_name: "シナノゴールド"
//    `シナノゴールド` というリテラル型

// as const 無し
const b2 = {
    name: shinano_gold_name
    // => (property) name: string
    //     nameはstring型
}

// as const あり
const b3 = {
    name: shinano_gold_name  
    // => (property) name: "シナノゴールド"
    //    nameは `シナノゴールド` というリテラル型
} as const

 
また、書籍では実際のデータから型を取り出すために typeof + as const を使っていたので、こちらも試してみました。

// 配列データから型を取り出す
// 配列データに as const を指定して、配列データの型をタプル型にする
const b_data = ['シナノゴールド', '秋映'] as const
// => const b_data: readonly ["シナノゴールド", "秋映"]

type b_type = typeof b_data[number]
// => type b_type = "シナノゴールド" | "秋映"

 

p113 タプル型・配列型から型の取り出し

ここでは、タプル型から T[number] で型を取り出す場合、ユニオン型へと変換されることを学びました。

// 配列型
type ApplesArray = string[]

type c1 = ApplesArray[0]
// => type c1 = string

type c2 = ApplesArray[number]
// => type c2 = string


// タプル型
type ApplesTuple = ['フジ', 'シナノスイート']

type d1 = ApplesTuple[0]
// => type d1 = "フジ"

type d2 = ApplesTuple[number]
// => type d2 = "フジ" | "シナノスイート"

 
なお、解説で

この number というキーワードは、実はタプル型に対しても使用できます。タプル型に対して [number] で型を取り出すと、そのタプル型が抱えているすべての型のユニオン型が取得できます

書籍「手を動かしながら学ぶTypeScript」 p113

とあった部分について、 number がキーワードなら他には何を指定できるのかが気になりました。

調べてみたところ、以下の記載がありました。

T[number]というのは配列である T に対して number 型のプロパティ名でアクセスできるプロパティの型ですから、すなわち配列 T の要素の型ですね。

TypeScriptの型入門 - Qiita

 
もしかしたら number はキーワードではなく型かもしれないと思って調べたところ、以下の記事がありました。
タプル型 T において、なぜ T[number] はUnion型になるのかに関する考察

その記事からリンクされていたプルリクを見ると

Indexed access types of the form T[K], where T is some type and K is a type that is assignable to keyof T (or assignable to number if T contains a numeric index signature).

Static types for dynamically named properties by ahejlsberg · Pull Request #11929 · microsoft/TypeScript

とありました。

プルリクによれば、 number 型を指定したときだけ index signature によるアクセスとなるようです。
Index Signatures | TypeScript: Documentation - Object Types

ということで、 number はどちらかというと型であると理解しました。

 
ちなみに、index signatureには数字文字列 ('0') も使えるようでした。
Index signature(インデックス型) - TypeScript Deep Dive 日本語版

// 配列型
type ApplesArray = string[]

type c3 = ApplesArray['0']
// => type c3 = string


// タプル型
type ApplesTuple = ['フジ', 'シナノスイート']

type d3 = ApplesTuple['0']
// => type d3 = "フジ"

 

p132 Mapped Types

書籍のp132では、ユニオン型がオブジェクトのキーとなるような例が記載されていました。

ただ、写経だけでは感覚がつかめなかったので、改めて試してみました。

// りんごの配列をタプル型にする
const appleNames = ['王林', '千秋', '秋映'] as const
// => const appleNames: readonly ["王林", "千秋", "秋映"]

// typeof Tuple[number] でリテラル型として取り出す
type AppleNames = typeof appleNames[number]
// => type AppleNames = "王林" | "千秋" | "秋映"

// Mapped Typesで、colorとparentsプロパティを持つりんご型を作る
type AppleTypes = {
    [key in AppleNames]: { color: string, parents: string[] }
}

// OKな例
const e1: AppleTypes = {
    '王林': { color: '黄', parents: ['ゴールデンデリシャス', '印度'] },
    '千秋': { color: '赤', parents: ['東光', 'フジ'] },
    '秋映': { color: '黄', parents: ['千秋', 'つがる'] },
}

// NGな例:キーが足りない
const e2: AppleTypes = {
    '王林': { color: '黄', parents: ['ゴールデンデリシャス', '印度'] },
    // => Type '{ 王林: { color: string; parents: string[]; }; }' 
    //    is missing the following properties from type 'AppleTypes': 秋映, 千秋
}

// NGな例:キーが多い
const e3: AppleTypes = {
    '王林': { color: '黄', parents: ['ゴールデンデリシャス', '印度'] },
    '千秋': { color: '赤', parents: ['東光', 'フジ'] },
    '秋映': { color: '黄', parents: ['千秋', 'つがる'] },
    'トキ': { color: '黄', parents: ['王林', 'フジ'] },
    // => Object literal may only specify known properties, and ''トキ'' does not exist in type 'AppleTypes'.
}

// NGな例:オブジェクトの型が違う
const e4: AppleTypes = {
    '王林': { color: '黄', parents: ['ゴールデンデリシャス', '印度'] },
    '千秋': { color: '赤', parents: ['東光', 'フジ'] },
    '秋映': { color: 1, parents: ['千秋', 'つがる'] },
    // => The expected type comes from property 'color' 
    //    which is declared here on type '{ color: string; parents: string[]; }'
}

// NGな例:値であるオブジェクトのプロパティが足りない
const e5: AppleTypes = {
    '王林': { color: '黄', parents: ['ゴールデンデリシャス', '印度'] },
    '千秋': { color: '赤', parents: ['東光', 'フジ'] },
    '秋映': { color: '黄' },
    // => Property 'parents' is missing in type '{ color: string; }' 
    //    but required in type '{ color: string; parents: string[]; }'.
}

 
なお、Mapped Typesはinterfaceでは使えないようです。
interfaceとtypeの違い | TypeScript入門『サバイバルTypeScript』

// Mapped Typesはinterfaceでは使えない
interface IAppleTypes {
    [key in AppleNames]: { color: string, parents: string[] }
    // -> A mapped type may not declare properties or methods.
}

 

p185 列挙型(enum)の代替

書籍では enum について書かれていたもののの、列挙型を使うべきでないとも書かれていました。

その理由として、p185では

  • TypeScriptのコンセプトに合っていない
  • 数値列挙型は型安全でない

が挙げられていました。

手元でも後者について試してみたところ、たしかに型安全ではありませんでした。

enum AppleColor {
    Red,
    Yellow,
    Green,
}

const f1: AppleColor = 'フジ'
// => Type '"フジ"' is not assignable to type 'AppleColor'.

const f2: AppleColor = -1 // エラーにならない

 
また、書籍以外の理由でも、列挙型を使わないほうが良いと分かりました。
さようなら、TypeScript enum - 株式会社カブク | 株式会社カブク

 
書籍や上記記事では代替として as const + keyof + typeof が書かれていたので、手元でも試してみます。

なお、 typeofkeyofワンライナーで書いてあると分かりづらいため、このメモでは都度型として定義しておきます。

const appleColorMap = {
    Red: 0,
    Yellow: 1,
    Green: 2,
} as const

// type TAppleColor = typeof appleColorMap[keyof typeof appleColorMap]
// が長いので、分解して書く
// なお、型は `type TAppleColor = 0 | 2 | 1` になる

// typeof でオブジェクトから型を作成
type TypeOfAppleColorMap = typeof appleColorMap
// => type TypeOfAppleColorMap = {
//        readonly Red: 0;
//        readonly Yellow: 1;
//        readonly Green: 2;
//    }

// keyof で型から各プロパティのユニオン型を取り出す
type KeyOfTypeOfAppleColorMap = keyof TypeOfAppleColorMap
// => type KeyOfTypeOfAppleColorMap = "Red" | "Yellow" | "Green"

// TypeOfAppleColorMap型から、キーがKeyOfTypeOfAppleColorMap(Red/Yellow/Green)である各値(0/1/2)を取り出す
type TAppleColor = typeof appleColorMap[KeyOfTypeOfAppleColorMap]
// => type TAppleColor = 0 | 2 | 1


// 型で定義されていない値を実際に使ってみる
const f3: TAppleColor = -1
// => Type '-1' is not assignable to type 'TAppleColor'

 

p225 Conditional Types

Conditional Types については

型定義における条件分岐を実現するための機能です。

書籍「手を動かしながら学ぶTypeScript」 p225

Webの記事だとこのあたりが参考になりました。

 
上記記事を参考にしながら、手元でも試してみます。

// nameプロパティを持っていなければ never にする例
type AppleCondition<T> = T extends { name: unknown } ? T : never

type g1 = AppleCondition<{ name: 'シナノホッペ' }>
// => type g1 = {
//       name: 'シナノホッペ';
//    }

type g2 = AppleCondition<{ color: '黄' }>
// => type g2 = never


// 2つ目の型引数を除いた別の型を作る例
type AppleDiff<T, U> = T extends U ? never : T

// りんご配列から、りんご三兄弟にいないフジを除いた型を作る
const apples = ['フジ', '秋映', 'シナノスイート', 'シナノゴールド'] as const
type りんご三兄弟 = AppleDiff<typeof apples[number], 'フジ'>
// => type りんご三兄弟 = "シナノゴールド" | "秋映" | "シナノスイート"

 

ソースコード

Githubに上げました。
https://github.com/thinkAmi-sandbox/shakyo_awesome-typescript-book

プルリクは各章ごとに作りました。

 
また、手元でTypeScript Sandboxで書いてみて、CodeSandboxにエクスポートしたコードはこちらです。
https://codesandbox.io/s/e24w4q?file=/index.ts