Slide 1

Slide 1 text

0 TypeScript 5.0 const 型パラメータの使い道 2023-04-07 第39回NearMe技術勉強会 @yujiosaka

Slide 2

Slide 2 text

1 TypeScript とは

Slide 3

Slide 3 text

2 Type https://github.com/type-challenges/type-challeng es TypeScript tips and Tricks with Matt https://www.youtube.com/watch?v=hBk4nV7q6- w TypeScript ⊇ JavaScript // これは TypeScript function add1(a: number, b: number): number { return a + b; } // 実はこれも TypeScript function add2(a, b) { return a + b; } https://typescript-jp.gitbook.io/deep-dive/

Slide 4

Slide 4 text

3 ● JavaScript で型システムが使える(実際に型を利用するかどうかは開発者の自由) ● JavaScript の将来のバージョンで計画されている機能が使える なぜ TypeScript なのか? → プログラムを実行する前にエラーが検出できる → 型はそれ自体が良質なドキュメントになる → オプショナルチェーン → デコレータ → etc. a?.b?.c // オプショナルチェーン a?.b?.c // オプショナルチェーン @decorator class A {...} // デコレータ https://trends.google.co.jp/trends/explore?date=2013-03-25%202023-03-25&q=TypeScript

Slide 5

Slide 5 text

4 TypeScript の型システム

Slide 6

Slide 6 text

5 型推論 vs. 型宣言 // 型宣言 let foo: number = 123; foo = “456”; // 型推論 let bar = 123; bar = “456”; https://typescript-jp.gitbook.io/deep-dive/getting-started/why-typescript

Slide 7

Slide 7 text

6 構造型 ≠ 宣言型 interface Point2D { x: number; y: number; } interface Point3D { x: number; y: number; z: number; } const point2D: Point2D = { x: 0, y: 10 } const point3D: Point3D = { x: 0, y: 10, z: 20 } function iTakePoint2D(point: Point2D) { /* なんらかの処理 */ } iTakePoint2D(point2D); // 全く同じ構造なので問題なし iTakePoint2D(point3D); // 追加のプロパティがあっても問題なし iTakePoint2D({ x: 0 }); https://typescript-jp.gitbook.io/deep-dive/getting-started/why-typescript

Slide 8

Slide 8 text

7 ジェネリック型 // ジェネリック型なし class QueueNumber { private data = []; push(item: number) { super.push(item); } pop(): number { return this.data.shift(); } } // ジェネリック型あり class Queue { private data: T[] = []; push(item: T) { this.data.push(item); } pop(): T | undefined { return this.data.shift(); } } const queue = new Queue(); queue.push(0); queue.push("1"); https://typescript-jp.gitbook.io/deep-dive/getting-started/why-typescript

Slide 9

Slide 9 text

8 TypeScript のヤバい型システム

Slide 10

Slide 10 text

9 TypeScript の型システムはチューリング完全 https://github.com/microsoft/TypeScript/issues/14833

Slide 11

Slide 11 text

10 偶然できてしまったチューリングマシン等 https://beza1e1.tuxen.de/articles/accidentally_turing_complete.html

Slide 12

Slide 12 text

11 ● 一生コンパイルが終わらない型定義を書くことができてしまう(停止性問題) ● 1つの言語のために、2つのチューリングマシンをマスターしなければならない ● JavaScript を型安全にするための必要悪のようなもの 必ずしも良いこと尽くめではない → 後で意味が分かると思います

Slide 13

Slide 13 text

12 リテラル型 // let または const でプリミティブ型を宣言 let version = 1; version === 2; const readonlyVersion = 1; readonlyVersion === 2; // const アサーション「あり」または「なし」でオブジェクトを宣言 const user = { name: "yujiosaka" }; user.name === "Yuji Isobe"; const reaonlyUser = { name: "yujiosaka" } as const; reaonlyUser.name === "Yuji Isobe"; // チェスゲームの作成 type File = "A" | "B" | "C" | "D" | "E" | "F" | "G" | "H"; type Rank = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8; let rank: Rank = 0;

Slide 14

Slide 14 text

13 型チャレンジ

Slide 15

Slide 15 text

14 Easy: If type If = C extends true ? T : F https://github.com/type-challenges/type-challenges/blob/main/questions/00268-easy-if/README.md

Slide 16

Slide 16 text

15 Medium: Deep readonly type DeepReadonly = { readonly [key in keyof T]: keyof T[key] extends never ? T[key] : DeepReadonly } https://github.com/type-challenges/type-challenges/blob/main/questions/00009-medium-deep-readonly/README .md

Slide 17

Slide 17 text

16 Hard: camelize type Camelize = T extends object ? { [K in keyof T as CamelCase]: T[K] extends unknown[] ? ( CamelizeArray ) : Camelize } : T type CamelizeArray = T extends [infer First, ...infer Rest] ? ( [Camelize, ...CamelizeArray] ) : T type CamelCase = T extends `${infer First}${'_'}${infer Second}${infer Rest}` ? ( `${First}${CamelCase<`${Uppercase}${Rest}`>}` ) : T https://github.com/type-challenges/type-challenges/blob/main/questions/01383-hard-camelize/README.md

Slide 18

Slide 18 text

17 Extreme: Query string parser type GetKeyAndValue = S extends `${infer first}=${infer rest}` ? first extends keyof O ? O[first] extends any[] ? rest extends O[first][number] ? O : { [key in (keyof O) | first]: key extends first ? [...O[first], rest] : O[key] } : rest extends O[first] ? O : { [key in (keyof O) | first]: key extends first ? [O[first], rest] : O[key] } : { [key in (keyof O) | first]: key extends first ? rest : key extends keyof O ? O[key] : never } : S extends "" ? O : { [key in (keyof O) | S]: key extends S ? true : key extends keyof O ? O[key] : never } type ParseQueryString = S extends `${infer first}&${infer rest}` ? ParseQueryString> : GetKeyAndValue https://github.com/type-challenges/type-challenges/blob/main/questions/00151-extreme-query-string-parser/README.md

Slide 19

Slide 19 text

18

Slide 20

Slide 20 text

19 変更点 ● デコレータ ● const 型パラメータ ● extends 複数設定ファイル対応 ● enums 型安全化 ● etc. https://github.com/microsoft/TypeScript/issues/30680#issuecomment-1161619377 https://github.com/microsoft/TypeScript/issues/30680#issuecomment-1161619377

Slide 21

Slide 21 text

20 const 型パラメータ

Slide 22

Slide 22 text

21 TypeScript < 5.0 type HasNames = { names: readonly string[] }; function getNamesExactly(arg: T): T["names"] { return arg.names; } // as const アサーションなし const names1 = getNamesExactly({ names: ["Alice", "Bob", "Eve"] }); // as const アサーションあり const names2 = getNamesExactly({ names: ["Alice", "Bob", "Eve"] } as const);

Slide 23

Slide 23 text

22 TypeScript >= 5.0 type HasNames = { names: readonly string[] }; function getNamesExactly(arg: T): T["names"] { return arg.names; } // as const アサーションあり const names3 = getNamesExactly({ names: ["Alice", "Bob", "Eve"] });

Slide 24

Slide 24 text

23 2022年6月

Slide 25

Slide 25 text

24 オープンソースプロジェクト https://github.com/yujiosaka/sequelize-revision

Slide 26

Slide 26 text

25 Sequelize(ORマッパー)で定義されたモデルのへの変更履歴を保存する フォーク元(Sequelize Paper Trail)と互換性を保ちつつ、以下の機能を改善 ● TypeScript で全て書き直し、型チェックをサポート ● sequelize-typescript をサポート ● Node.js >= 8.0 と Async Hooks に対応 ● 単体テストのカバレッジ2倍 ● etc. 機能

Slide 27

Slide 27 text

26 Sequelize Revision の問題

Slide 28

Slide 28 text

27 設定が柔軟すぎる const sequelizeRevision = new SequelizeRevision(sequelize, { UUID: false, useJsonDataType: false, underscored: false, underscoredAttributes: false, revisionAttribute: "revision", userModel: "User", userIdAttribute: "userId", enableRevisionChangeModel: false, }); const [Revision] = sequelizeRevision.defineModels(); interface Revision extends Model<...> { id: number; document: string; documentId: number; operation: string; revision: number; userId: number; createdAt: Date; updatedAt: Date; } 設定に応じて全く違う変更履歴モデル(Revision, RevisionChange)が定義される

Slide 29

Slide 29 text

28 設定が柔軟すぎる const sequelizeRevision = new SequelizeRevision(sequelize, { UUID: true, useJsonDataType: true, underscored: true, underscoredAttributes: true, revisionAttribute: "version", revisionIDAttribute: "versionId", // userModel: "User", // userIdAttribute: "userId", enableRevisionChangeModel: true, }); const [Revision, RevisionChange] = sequelizeRevision.defineModels(); interface Revision extends Model<...> { id: string; document: object; document_id: string; operation: string; version: number; // userId declaration is gone created_at: Date; updated_at: Date; } interface RevisionChange extends Model<...> { id: string; version_id: string; path: object; document: object; diff: object; created_at: Date; updated_at: Date; } 設定に応じて全く違う変更履歴モデル(Revision, RevisionChange)が定義される

Slide 30

Slide 30 text

29 Sequelize Revision 型チャレンジ

Slide 31

Slide 31 text

30 静的なフィールドを定義する class SequelizeRevision { constructor( sequelize: Sequelize, options?: O ) { // Initialize the instance } public defineModels(): [ModelStatic>] { const Revision = this.sequelize.define>( // Define Revision model return [Revision]; } // Other functions } interface Revision extends Model< InferAttributes>, InferCreationAttributes> > { // id: string | number; model: string; // document: string | object; operation: string; // revision: number; // documentId: string | number; // userId: string | number; // createdAt: Date; // updatedAt: Date; }

Slide 32

Slide 32 text

31 設定に応じて型を変更する class SequelizeRevision { constructor( sequelize: Sequelize, options?: O ) { // Initialize the instance } public defineModels(): [ModelStatic>] { const Revision = this.sequelize.define>( // Define Revision model return [Revision]; } // Other functions } interface Revision extends Model< InferAttributes>, InferCreationAttributes> > { id: O["UUID"] extends true ? string : number; model: string; document: O["useJsonDataType"] extends true ? object : string; operation: string; // revision: number; // documentId: string | number; // userId: string | number; // createdAt: Date; // updatedAt: Date; }

Slide 33

Slide 33 text

32 動的にフィールド名を変更する interface Revision extends Model< InferAttributes>, InferCreationAttributes> > { id: O["UUID"] extends true ? string : number; model: string; document: O["useJsonDataType"] extends true ? object : string; operation: string; [Revision in O["revisionAttribute"] extends string ? O["revisionAttribute"] : "revision"]: number; // documentId: string | number; // userId: string | number; // createdAt: Date; // updatedAt: Date; } class SequelizeRevision { constructor( sequelize: Sequelize, options?: O ) { // Initialize the instance } public defineModels(): [ModelStatic>] { const Revision = this.sequelize.define>( // Define Revision model return [Revision]; } // Other functions }

Slide 34

Slide 34 text

33 設定に応じてフィールド名を変更する type RevisionAttributes = { id: O["UUID"] extends true ? string : number; model: string; document: O["useJsonDataType"] extends true ? object : string; operation: string; // documentId: string | number; // userId: string | number; // createdAt: Date; // updatedAt: Date; } & { [Revision in O["revisionAttribute"] extends string ? O["revisionAttribute"] : "revision"]: number; } type RevisionCreationAttributes = Optional, "id">; type Revision = Model, RevisionCreationAttributes> & RevisionAttributes; class SequelizeRevision { constructor( sequelize: Sequelize, options?: O ) { // Initialize the instance } public defineModels(): [ModelStatic>] { const Revision = this.sequelize.define>( // Define Revision model return [Revision]; } // Other functions }

Slide 35

Slide 35 text

34 設定に応じてフィールド名をキャメルケース化する type RevisionAttributes = { id: O["UUID"] extends true ? string : number; model: string; document: O["useJsonDataType"] extends true ? object : string; operation: string; } & { [Revision in O["revisionAttribute"] extends string ? O["revisionAttribute"] : "revision"]: number; } & { [DocumentId in O["underscoredAttributes"] extends true ? CamelToSnakeCase<"documentId"> : "documentId"]: O["UUID"] extends true ? string : number; } & { [UserId in O["userModel"] extends string ? O["userIdAttribute"] extends string ? O["underscoredAttributes"] extends true ? CamelToSnakeCase : O["userIdAttribute"] : O["underscoredAttributes"] extends true ? CamelToSnakeCase<"userId"> : "userId" : never]: O["UUID"] extends true ? string : number; } & { [CreatedAt in O["underscoredAttributes"] extends true ? CamelToSnakeCase<"createdAt"> : "createdAt"]: Date; } & { [UpdatedAt in O["underscoredAttributes"] extends true ? CamelToSnakeCase<"updatedAt"> : "updatedAt"]: Date; } type RevisionCreationAttributes = Optional, "id">; type Revision = Model, RevisionCreationAttributes> & RevisionAttributes; [UserId in O["userModel"] extends string ? O["userIdAttribute"] extends string ? O["underscoredAttributes"] extends true ? CamelToSnakeCase : O["userIdAttribute"] : O["underscoredAttributes"] extends true ? CamelToSnakeCase<"userId"> : "userId" : never]: O["UUID"] extends true ? string : number; class SequelizeRevision { constructor( sequelize: Sequelize, options?: O ) { // Initialize the instance } public defineModels(): [ModelStatic>] { const Revision = this.sequelize.define>( // Define Revision model return [Revision]; } // Other functions }

Slide 36

Slide 36 text

35 完成 import type { Model, Optional } from "sequelize"; import type { SequelizeRevisionOptions } from "./options"; import type { CamelToSnakeCase } from "./util-types"; type TimestampAttributes = { [CreatedAt in O["underscoredAttributes"] extends true ? CamelToSnakeCase<"createdAt"> : "createdAt"]: Date; } & { [UpdatedAt in O["underscoredAttributes"] extends true ? CamelToSnakeCase<"updatedAt"> : "updatedAt"]: Date; }; type MetaDataAttributes = { [Field in keyof O["metaDataFields"]]: any; }; type RevisionAttributes = { id: O["UUID"] extends true ? string : number; model: string; document: O["useJsonDataType"] extends true ? object : string; operation: string; } & { [Revision in O["revisionAttribute"] extends string ? O["revisionAttribute"] : "revision"]: number; } & { [DocumentId in O["underscoredAttributes"] extends true ? CamelToSnakeCase<"documentId"> : "documentId"]: O["UUID"] extends true ? string : number; } & { [UserId in O["userModel"] extends string ? O["userIdAttribute"] extends string ? O["underscoredAttributes"] extends true ? CamelToSnakeCase : O["userIdAttribute"] : O["underscoredAttributes"] extends true ? CamelToSnakeCase<"userId"> : "userId" : never]: O["UUID"] extends true ? string : number; } & MetaDataAttributes & TimestampAttributes; type RevisionCreationAttributes = Optional, "id">; export type Revision = Model, RevisionCreationAttributes> & RevisionAttributes; type RevisionChangeAttributes = { id: O["UUID"] extends true ? string : number; path: string; document: O["useJsonDataType"] extends true ? object : string; diff: O["useJsonDataType"] extends true ? object : string; } & { [RevisionId in O["revisionIdAttribute"] extends string ? O["underscoredAttributes"] extends true ? CamelToSnakeCase : O["revisionIdAttribute"] : O["underscoredAttributes"] extends true ? CamelToSnakeCase<"revisionId"> : "revisionId"]: O["UUID"] extends true ? string : number; } & TimestampAttributes; type RevisionChangeCreationAttributes = Optional, "id">; export type RevisionChange = Model, RevisionChangeCreationAttributes> & RevisionChangeAttributes; class SequelizeRevision { constructor( sequelize: Sequelize, options?: O ) { // Initialize the instance } public defineModels(): O["enableRevisionChangeModel"] extends true ? [ModelStatic>, ModelStatic>] : [ModelStatic>] { const Revision = this.sequelize.define>( // Define Revision model const RChange = this.sequelize.define>( // Define RevisionChange model return [Revision, RChange]; } // Other functions }

Slide 37

Slide 37 text

36 チャレンジ成功…?

Slide 38

Slide 38 text

37 あと一歩 const sequelizeRevision = new SequelizeRevision(sequelize, { underscored: true, underscoredAttributes: true, userModel: "User", userIdAttribute: "user_id", enableRevisionChangeModel: true, }); const [Revision] = sequelizeRevision.defineModels(); const revision = (await Revision.findOne()) || new Revision(); revision.document_id = 1; revision.user_id = 2; // トランスパイルが通るはず revision.invalid_attribute = 3;

Slide 39

Slide 39 text

38 こうすれば問題ない const sequelizeRevision = new SequelizeRevision(sequelize, { readonly underscored: true, readonly underscoredAttributes: true, readonly userModel: "User", readonly userIdAttribute: "user_id", readonly enableRevisionChangeModel: true, }); const [Revision] = sequelizeRevision.defineModels(); const revision = (await Revision.findOne()) || new Revision(); revision.document_id = 1; revision.user_id = 2; revision.invalid_attribute = 3; const sequelizeRevision = new SequelizeRevision(sequelize, { underscored: true, underscoredAttributes: true, userModel: "User", userIdAttribute: "user_id", enableRevisionChangeModel: true, } as const); const [Revision] = sequelizeRevision.defineModels(); const revision = (await Revision.findOne()) || new Revision(); revision.document_id = 1; revision.user_id = 2; revision.invalid_attribute = 3;

Slide 40

Slide 40 text

39 呼び出し元全てに const アサーションが必要

Slide 41

Slide 41 text

40 const 型パラメータの出番

Slide 42

Slide 42 text

41 型パラメータに const 制約を付与する class SequelizeRevision { constructor( sequelize: Sequelize, options?: O ) { // Initialize the instance } class SequelizeRevision { constructor( sequelize: Sequelize, options?: O ) { // Initialize the instance }

Slide 43

Slide 43 text

42 const アサーションを取り払っても動作する const sequelizeRevision = new SequelizeRevision(sequelize, { underscored: true, underscoredAttributes: true, userModel: "User", userIdAttribute: "user_id", enableRevisionChangeModel: true, } as const); const [Revision] = sequelizeRevision.defineModels(); const revision = (await Revision.findOne()) || new Revision(); revision.document_id = 1; revision.user_id = 2; revision.invalid_attribute = 3; const sequelizeRevision = new SequelizeRevision(sequelize, { underscored: true, underscoredAttributes: true, userModel: "User", userIdAttribute: "user_id", enableRevisionChangeModel: true, }); const [Revision] = sequelizeRevision.defineModels(); const revision = (await Revision.findOne()) || new Revision(); revision.document_id = 1; revision.user_id = 2; revision.invalid_attribute = 3;

Slide 44

Slide 44 text

43 ● const アサーション(as const)を書き忘れて動作が変わってしまうことがない ● ライブラリのような、どのように呼び出されるか保証できない場合に特に有用 const 型パラメータのユースケース

Slide 45

Slide 45 text

44 素朴な疑問

Slide 46

Slide 46 text

45 ● 最初はつまづくと思うが、慣れればそこまで苦ではない(実はちょっと楽しい) ● 使用しているライブラリが型安全であれば、気にしなくても問題ない ● ライブラリ提供者ではなく、ただの使用者であれば、時間をかけるメリットもそんなにない しかし、もし自分がライブラリ提供者側にまわった場合は、 誰がそのライブラリを使うようになるのかは分からないので、 責任を持って安全な型を提供するように気をつけましょう。 毎回ここまでしないといけないのか?

Slide 47

Slide 47 text

46 Type https://github.com/type-challenges/type-challenges TypeScript tips and Tricks with Matt https://www.youtube.com/watch?v=hBk4nV7q6-w 参考

Slide 48

Slide 48 text

47 Thank you