Upgrade to PRO for Only $50/Year—Limited-Time Offer! 🔥

TypeScript

Recruit
August 10, 2023

 TypeScript

2023年度リクルート エンジニアコース新人研修の講義資料です

Recruit

August 10, 2023
Tweet

More Decks by Recruit

Other Decks in Technology

Transcript

  1. 自己紹介 • 名前:雫石 卓耶(しずくいし たくや)(@sititou70) • 2021年に新卒でリクルートに入社しました • 所属:横断エンジニアリング部 アプリケーション・ソリューショ ン・グループ(ASG)

    • 普段:TypeScript、React、Next.jsでWebアプリケーションを書 いています • 趣味:型パズルなど • よろしくお願いします 2 フローリングの アイコンが私
  2. Alt JS:JavaScriptにコンパイルされる(2/3) • いろいろあるAlt JS ◦ Flow, CoffeeScript, Haxe, PureScript,

    Reason… • ブラウザで動かせる言語は基本的にJSだけだから • 一時期はどのAlt JSが天下を取るかという状況だったが、最近はTSに軍配が上がりつ つある様子 9 Alt JSのリストの参考:https://github.com/jashkenas/coffeescript/wiki/List-of-languages-that-compile-to-JS#javascript-extensions
  3. Alt JS:JavaScriptにコンパイルされる(3/3) • 古いブラウザでも動くようにコンパイルすることもできます • Downlevelingといいます ◦ 例:async / awaitはそのままではIEでは動作しないが、TSが頑張ってコードを変換

    してくれる • もちろん、「まったくDownlevelingしない」ということもできます ◦ 例:tsは型検査だけで、コンパイルはBabelなど別のツールに任せる 10
  4. ミニ演習1 1. TS Playgroundを開きます 2. TS Config -> Type Checking

    -> strictが有効になっていることを確認します 3. 上記のコードを実際にTS Playgroundへコピペしてみてください ◦ 実行できますか ◦ コンパイルの結果、どのようなJSになりますか ▪ 右側の.JSタブから確認します 13 const msgString = "hello world"; console.log("現在のメッセージは" + msgString.length + "文字です");
  5. JSは動的な検査を行う言語(2/2) • または、次のように間違えたとします • こちらは実行してもエラーが出ません • そういう仕様なので 16 const msgString

    = 12345; console.log("現在のメッセージは" + msgString.length + "文字です"); 現在のメッセージはundefined文字です
  6. TSで書いてみる • すると、静的型検査で怒られました • ソースコードを実行する前に、静的解析によってエラーを発見できました 19 const msg: string =

    null; console.log("現在のメッセージは" + msg.length + "文字です"); 型注釈:「msgは文字列が入る変数」とい うのをTSに教えている
  7. ありがたみが薄い? • 今回のコードは簡単だったのでパッと見て不具合がわかった • しかしこれが何万行もあったら……? ◦ ※実際、プロジェクトにおけるTSのコードは数万行になりうる • TSは、プログラムの各部分を、それが計算する値の種類によって分類することにより、 プログラムがある種の振る舞い(例:nullや数値の.lengthを参照するといった動作)を起

    こさないことを検査してくれる*1 • 人の目で見るより、TSにやってもらったほうが効率的 21 *1:Pierce, Benjamin C. 型システム入門 プログラミング言語と型の理論. 株式会社 オーム社, 2013.(以降TAPLの略称で参照)の p.1にある型システムの定義から一部表現を借りています
  8. 注意:動的な検査が望ましい場合もある • 複雑な静的型検査をやろうとすると ◦ 難しい型パズルを書くことになったり ▪ そのコードを後の人が見てわからないということになったり ◦ そもそも型システムの表現力が足りず実現不可能だったり •

    それよりは動的に検査してしまって、素直にエラー画面などを表示したほうが簡単という 場合もあります • やりたいことに応じて適切な手段を取れるようになりましょう 22
  9. 型注釈 • TSに「ここは〜〜型ですよ」と教える方法 • いろいろなところに書けます 24 const a: string =

    "hoge"; let b: string; var c: string; function greet(name: string): string { return "hello, " + name; } class MyClass { e: string = ""; }
  10. しかし、わかりやすさのために型注釈を書くこともある 26 // 自明なので注釈は必須でない const num: number = 123; //

    dataの型が自明でないので注釈がほしい // 注釈があると、「長い処理」を書き間違ってしまったときにここでエラーが出る // 注釈を消して型推論に任せると、どこか別の場所でエラーが出てデバッグが大変になる かも const data = hogeArray.map(/* 長い処理 */).filter(/* 長い処理 */).reduce(/* さらにつづく...
  11. 基本的な型 27 参考:https://www.typescriptlang.org/docs/handbook/typescript-in-5-minutes-func.html#built-in-types const num: number = 123; const str:

    string = "hoge"; const bool: boolean = true; const sym: symbol = Symbol("fuga"); const nullObj: null = null; const undef: undefined = undefined; const bigint: bigint = 9007199254740991n;
  12. リテラル型 • 特定の値しか代入できない型 • 数値リテラル型 / 文字列リテラル型 / booleanリテラル型 28

    参考:https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#literal-types const oneHundred: 100 = 100; const helloMsg: "hello world" = "hello world"; const trueVal: true = true; const falseVal: false = false;
  13. 配列型 • タプル型:Array型の一種 ◦ 各要素の型、順番、個数を正確に把握している型 31 const array1: number[] =

    [1, 2, 3]; // または const array2: Array<number> = [1, 2, 3]; const array3: [number, string, true] = [123, "hello", true];
  14. 関数型(1/2) • 型は次のように表される 32 (num: number, bool: boolean) => string

    // 引数がない時 () => string // 戻り値が無い時 (num: number, bool: boolean) => void
  15. 関数型(2/2) • 型注釈で使うとしたらこんなかんじ • 以下のように書くことも多いです。意味は同じです 33 const fn1: (num: number,

    bool: boolean) => string = (num, bool) => `${num}${bool}`; const fn2 = (num: number, bool: boolean): string => `${num}${bool}`; function fn3(num: number, bool: boolean): string { return `${num}${bool}`; }
  16. any型(3/3) • anyあるある ◦ 既存のJSプロジェクトにTSを導入している。とりあえず導入だけ済ませて、型は後 から書きたい ◦ JSのライブラリをTSで使いたいが、型定義が不足している。自分で定義を書くのは 時間がかかるのでいったんanyにしておきたい ◦

    複雑な型エラーが出ていてよくわからん!詳しい人、後で助けてくれ!!! • うまく使えば非常ハッチとして役立つ ◦ しかし「よくわからないから」で放置してしまうと…… 36
  17. 型エイリアス • 型に別名を付けられる • 慣例として、型名は大文字から始める場合が多いです 37 type MyName = string;

    const myName: MyName = "taro yamada"; type Circle = { pos: { x: number; y: number }; r: number }; const circle: Circle = { pos: { x: 1, y: 2 }, r: 3 };
  18. 演習1 • 問題1:次のuser変数に型注釈を付けてください 38 const user = { firstName: "太郎",

    lastName: "山田", age: 24, favoriteFoods: ["寿司", "ラーメン", "カレー"], hasProgrammingExperience: true, };
  19. 演習1 • 問題2:次のようなgetSelfIntroduction関数を書いてみてください ◦ 引数:1つ。先程のuserオブジェクト。引数名は任意 ◦ 戻り値: string ▪ 次のような自己紹介文。[

    ]の中は引数によって変化する部分 ▪ 私の名前は[姓][名]です。年齢は[年齢]歳です。好きな食べ物は[〜〜と〜〜 と〜〜(〜〜には好きな食べ物配列の各要素が入る。要素数だけ繰り返す)] です。プログラミングの経験[があります or はありません] • 問題3:getSelfIntroduction関数を実際に実行し、consoleに自己紹介文を出力してみ てください 39
  20. Class 41 インターフェースの例の出典:https://ja.wikipedia.org/wiki/%E3%82%A4%E3%83%B3%E3%82%BF%E3%83%95%E3%82%A7%E3%83%BC%E3%82%B9_(%E6%8A%BD%E8%B1%A1%E5%9E%8B) interface Flyable { fly(): void; } class

    Bird implements Flyable { name: string; constructor(name: string) { this.name = name; } fly() { console.log(`${this.name}「パタパタ」`); } } const bat: Bird = new Bird("コウモリ");
  21. Interface vs 型エイリアス(2/2) • わりとその場のノリで使い分けています • Interfaceは拡張される。型エイリアスはされない(エラーになる • ライブラリの型がinterfaceで書かれており、ユーザー側でそれを拡張する……みたいな 場面もたまにある

    44 他の細かい違いは:https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#differences-between-type-aliases-and-interfaces interface Hoge { a: number; } interface Hoge { b: string; } どっちも同じ interface Hoge { a: number; b: string; }
  22. Union • 「または」の意味 • 合併型*1、バリアント型*2、和型*3とも • Stringのリテラル型と併用すると、列挙型のように扱うこともできる • (これとは別にenum構文もありますが、あまり利用されません) 45

    *1:Unionと同じ意味。*2:タグ付きのような「互いに素」なUnionを特に指す。*3:2つの型に対するバリアント型をいう。TAPL, 11.10節, p.108より type NumOrStr = string | number; type MaybeStr = string | null | undefined; type Colors = "red" | "blue" | "green" | "yellow";
  23. 型の絞り込み(1/2) • Union型は、if文やswitch文によって値をチェックしていくことで、より詳細な型へ絞り込 まれます 50 function fn(a?: number) { //

    ここでaはnumber | undefined型。以下はおこられる Math.sqrt(a); if (a === undefined) return; // ここではnumber型。以下はおこられない Math.sqrt(a); }
  24. 型の絞り込み(2/2) • 他にも以下のような構文で絞り込まれます ◦ typeof x === “string” ◦ x

    instanceof HogeClass ◦ リテラル型の同値確認( if (x === “some message”) ) ◦ obj.x や obj[“x”] や x in objによるプロパティの存在確認 • 上記だけでは十分でない場合、ユーザー定義の型ガードを使用する手もあります ◦ 詳しくは割愛。必要になったら調べましょう 51
  25. readonly • 書き換え操作を型検査で抑止します ◦ ランタイムのJSからは書き換え可能 52 type Obj = {

    readonly prop: string; }; const obj: Obj = { prop: "hello", }; obj.prop = "goodbye"; // おこられる
  26. unknown型 / never型 • unknown型 ◦ どのような値も代入できる ◦ できる操作は少ない:よく分からない値なので迂闊に扱えない ▪

    any型の値はそこから何でもできた ▪ unknown型の値は、それを具体的な値に絞り込まないと何もできないので安 全 • never型 ◦ どのような値も代入できない ◦ 型の絞り込みの結果、到達不能な部分などで現れる 53
  27. ジェネリクス(2/4) • 引数をそのまま返すid関数を作りたいとします • すべての型に対してid関数のバリエーションを作るのは面倒 55 const id = (arg)

    => arg; const idBoolean = (arg: boolean): boolean => arg; const idNumber = (arg: number): number => arg; const idString = (arg: string): string => arg; // ...
  28. ジェネリクス(4/4) • ジェネリクスを使うと、以下のようにできます 57 // 型変数Tで引数の型をキャプチャ const id = <T>(arg:

    T): T => arg; // resultはnumber型 const result = id<number>(123); // または型推論に任せて const result = id(123);
  29. 演習2 • 問題1:以下のgetUserName関数を完成させてください ◦ ヒント:途中で「object & Record<"name", unknown>」という型を見るかもしれませ ん。これは「{ name:

    unknown }」のことです ▪ 「&」の意味については後述します 58 type User = { name: string }; // User型が与えられれば名前を返す。そうでなければ undefinedを返す function getUserName(maybeUser: unknown): string | undefined { // TODO: ここでmaybeUserを絞り込む return maybeUser.name; }
  30. 演習2 • 問題2:以下のprintSelfIntroduction関数で使用されているSubject型を書いてみてくだ さい。関数の仕様は次ページにあります 59 function printSelfIntroduction(subject: Subject) { switch

    (subject.type) { case "name": console.log(`私の名前は${subject.payload}です`); return; case "favorite_food": console.log(`私の好きな食べ物は${subject.payload.join("と")}です`); return; } }
  31. 演習2 • 引数 ◦ subject: 自己紹介の主題。typeとpayloadの2つのプロパティをもつ ▪ payload: typeによって変わる ▪

    type: 文字列。”name”または”favorite_food” • “name”: payloadには名前(文字列)が入る。 • “favorite_food”: payloadには好きな食べ物の名前の配列(文字列の配 列)が入る。 • 戻り値 ◦ なし 60
  32. 部分型関係(1/2) • 例えば次のような型があります • Animal型の変数にBird型の値を代入しても安全です • Javaの教科書とかでよく見るやつですね 63 type Animal

    = { name: string }; type Bird = { name: string; wings: "翼" }; type Dog = { name: string; forefoot: "前足" }; const bird: Bird = { name: "スズメ", wings: "翼" }; const animal: Animal = bird; console.log(`この動物は${animal.name}です`);
  33. 部分型関係(2/2) • S型の値をT型の値として扱っても安全であるとき*1 ◦ SはTの部分型、TはSの上位型です ◦ SとTは部分型関係にあります ▪ サブタイプ関係、is-a関係とも ◦

    S <: Tと書きます(紙面の節約のため。TSの文法ではない) • 例: ◦ BirdはAnimalの部分型、AnimalはBirdの上位型 ◦ BirdとAnimalは部分型関係にあります ◦ Bird <: Animal • 再掲:部分型の値は上位型の値として扱っても安全 64 参考:principle of safe substitution(安全代入の原則、または安全代用の原則)、TAPL 15.1節。参考:subsumption principle(包摂原則)、Harper, Robert. Practical foundations for programming languages. Cambridge University Press(PFPL), 2016., Chapter 24。*1:より正確には「型Sの任意の項が、型Tの項が期待されている文脈で安全に使用可能であるとき」、TAPL 15.1節より。
  34. 部分型関係の性質 • 反射的 ◦ Animal <: Animal ◦ Bird <:

    Bird ◦ Dog <: Dog • 推移的*1 ◦ Chihuahua <: Dog <: Animal ◦ ならば ◦ Chihuahua <: Animal • 前順序(pre-order)とも 65 参考:TAPL 15.2節 p.142。*1:TSの代入可能関係は一部で推移的ではないようなのでご注意ください。詳細:https://github.com/sititou70/ts-extends-hierarchy。また、部分型関係と代入可能関係の違いについて
  35. 配列の部分型関係 • S <: T • ならば • S[] <:

    T[] • です*1 67 *1:これは便利ですが安全ではありません。詳しくは:TypeScriptにおける配列の共変性
  36. 関数の部分型関係 • T1 <: S1 かつ S2 <: T2 •

    ならば • (S1) => S2 <: (T1) => T2 • です • ちょっと直感的ではないです。後の演習で試して理解するのが早いと思います 68 参考:TAPL 15.2節, p.144
  37. 部分型関係まとめ • 以下のようなエラーメッセージを見かけたら ◦ type '〜〜' is not assignable to

    type '〜〜' • まずは部分型関係を思い出してみましょう • エラーが出ている部分について、コードが安全かどうか確かめましょう ◦ 安全か:部分型の値を上位型の値として扱っているか 70
  38. アップキャスト • source as Targetのとき、Source <: Targetであること • 例:dog as

    Animal • 常に安全 ◦ Dogの値をAnimalとして扱っても何の問題もない • そもそも必要ないことも 73 // as Animalは必要ない const animal: Animal = dog as Animal;
  39. ダウンキャスト • source as Targetのとき、Target <: Sourceであること • 例:animal as

    Dog • 危険! ◦ Animalの値をDogとして扱う ◦ Dogにしかないプロパティにアクセスしたら実行時エラーになる • 先程の例のように、相当の理由と自信がある場合のみ使ってください 74 const myCanvas = document.getElementById("main_canvas") as HTMLCanvasElement;
  40. Intersection • 「かつ」の意味 • Unionの逆 • 交差型とも 77 *1:S-Inter3規則、TAPL p.161

    type A = number & unknown; // number type B = number & never; // never type C = { a: number } & { b: string }; // C <: { a: number } かつ C <: { b: string }であるようなCは?*1 // C = { a: number; b: string }
  41. 演習3 • 問題2:以下のコードを使って、関数の部分型関係を検証してください 79 type Animal = { name: string

    }; type Dog = { name: string; forefoot: "前足" }; const dog: Dog = { name: "イヌ", forefoot: "前足" }; let dogToAnimal = (dog: Dog): Animal => dog; // TODO: ここでdogToAnimalを、その部分型の関数で代用してみてください // ヒント:関数の部分型関係の定義より、代用する関数の型は次のようにできるでしょう // 引数の型:元の関数における引数の上位型 // 戻り値の型:元の関数における戻り値の部分型 dogToAnimal = ここを埋める; const animal: Animal = dogToAnimal(dog);
  42. 演習3 • 問題3:以下はそれぞれどのような型でしょうか。確かめてみてください 80 type D = { prop: {

    a: number } } & { prop: { b: string } }; type E = number & 12345; type F = "Love" & "Peace";
  43. 発展:Utility Types • 型の取り回しを便利にする型 • 型を受け取って型を返します*1 • たま〜〜〜に使います。今回は割愛 82 *1:型演算子とも。ただし高階の型演算子はTSにないです。工夫してそれっぽいものを実現することはできます。参考:https://github.com/gvergnaud/hotscript

    type A = Partial<{ a: number }>; // { a?: number } type B = Required<{ a?: number }>; // { a: number } type C = Pick<{ a: number; b: string }, "b">; // { b: string } type D = Omit<{ a: number; b: string }, "b">; // { a: number } type E = Readonly<{ a: number }>; // { readonly a: number; } // まだまだある
  44. モジュール • JSには複数のモジュールのスタイルがあります(歴史的事情) ◦ ES2015のモジュール ▪ 例:import hoge from “module-name”;

    ▪ ECMAScript 2015というバージョンで初めてモジュールに関する構文が仕様化されました ▪ それまではJSにモジュールという概念は無かった ◦ Common JSのモジュール ▪ 例:const hoge = require(“module-name”); ▪ ES2015以前から、Node.jsなどで使われていたスタイル ◦ 他にも色々 • JSの実行環境によってモジュールのスタイルが異なるのが現状 ◦ したがって、利用するモジュールローダー(モジュールを読み込む仕組み)も使い分ける必要があり ます 93
  45. TSのモジュール事情 • 基本はESスタイルで記述 • コンパイル時にランタイムで利用したいモジュールローダーを指定すると、うまいこと変換してく れます • tsconfig.jsonのmodule:利用したいモジュールローダーを指定 ◦ commonjs:Node.jsで実行したい場合など

    ◦ esnext:ブラウザの<script type=“module”>を使う場合など ◦ 他の値も選択可能だが、使うケースは正直少ない • 演習 ◦ importとexportを使って適当なTSのコードを書いてください ◦ Playgroundのmoduleの設定をcommonjsとesnextで切り替えたとき、生成されるJSはどの ように変化しますか 94
  46. ビルド • JavaScriptのビルドの歴史は割と闇が深い • 太古の昔 ◦ AltJSが流行る前は、開発者が書いた.jsファイルをそのままHTMLの<script>タグで読み 込んでいました • よりリッチな体験がwebに求められるにつれ、読み込まれるJSファイルは重厚長大化

    • これにともなって、様々な課題が浮上 ◦ JSファイルを分割したいが、<script>タグによる読み込み回数は増やしたくない ◦ 別言語(AltJS)で開発したい ◦ ダウンロードサイズ削減のため、JSファイルを圧縮したい ◦ etc… • いろいろなツールが開発され、吟味され、混乱もあり、いろいろあって …… 95
  47. 周辺のツールチェイン:TypeScript自体の便利機能 • TypeScript本体に付属しているtscというツール自体にも便利機能があります ◦ --noEmit : JSの生成を行なわずに、型検査のみを実行するオプション. CI等に組み 込むと便利 ◦

    --noUnusedLocals、--noUnusedParameters: 利用されていない変数を検知してエ ラーにするオプション. 冗長なコードの削減になる • その他にも、tscには様々なオプションが用意されています。公式ドキュメントを見てみる とよいでしょう 99
  48. 周辺のツールチェイン:Prettier • ソースコードを機械的にフォーマットするためのツール • 下記のような不毛な議論に左右されなくなります ◦ 「文字列リテラルはシングルクォート or ダブルクォート」 ◦

    「セミコロンを文の末尾につけるべきか」 • ファイルの保存時やコミット時に自動的にフォーマットが走る設定にしておくと便利です ◦ ファイルの保存時:VSCodeの設定 ◦ コミット時:Git Hookの設定、例えばHuskyを使うなど • Prettierは標準でTypeScriptに対応 101
  49. 参考文献 • TypeScriptの公式ドキュメント ◦ https://www.typescriptlang.org/docs/ • Pierce, Benjamin C. 型システム入門

    プログラミング言語と型の理論. 株式会社 オーム 社, 2013. ◦ TAPLの略称でも参照している 104
  50. 型パズルでもやって遊んでいてください(Level 1) • 以下のようなIf型を定義してください 105 type CaseIf1 = If<true, 'a',

    'b'> // 'a' type CaseIf2 = If<false, 'a', 'b'> // 'b' 出典:https://github.com/type-challenges/type-challenges/blob/main/questions/00268-easy-if/README.md
  51. 型パズルでもやって遊んでいてください(Level 2) • 以下のようなReverse型を定義してください 106 type CaseReverse1 = Reverse<['a', 'b']>

    // ['b', 'a'] type CaseReverse2 = Reverse<['a', 'b', 'c']> // ['c', 'b', 'a'] 出典:https://github.com/type-challenges/type-challenges/blob/main/questions/03192-medium-reverse/README.md
  52. 型パズルでもやって遊んでいてください(Level 4) • 以下のような、指定した数までの素数に評価されるPrimes型を定義してください • 入力される数値の範囲は自身で定義して構いません 108 type CasePrimes1 =

    Primes<300> // [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97, 101, 103, 107, 109, 113, 127, 131, 137, 139, 149, 151, 157, 163, 167, 173, 179, 181, 191, 193, 197, 199, 211, 223, 227, 229, 233, 239, 241, 251, 257, 263, 269, 271, 277, 281, 283, 293]