Upgrade to Pro — share decks privately, control downloads, hide ads and more …

TypeScript 5.0 const 型パラメータの使い道

TypeScript 5.0 const 型パラメータの使い道

More Decks by NearMeの技術発表資料です

Other Decks in Programming

Transcript

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

    View Slide

  2. 1
    TypeScript とは

    View Slide

  3. 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/

    View Slide

  4. 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

    View Slide

  5. 4
    TypeScript の型システム

    View Slide

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

    View Slide

  7. 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

    View Slide

  8. 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

    View Slide

  9. 8
    TypeScript のヤバい型システム

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  13. 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;

    View Slide

  14. 13
    型チャレンジ

    View Slide

  15. 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

    View Slide

  16. 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

    View Slide

  17. 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

    View Slide

  18. 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

    View Slide

  19. 18

    View Slide

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

    View Slide

  21. 20
    const 型パラメータ

    View Slide

  22. 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);

    View Slide

  23. 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"] });

    View Slide

  24. 23
    2022年6月

    View Slide

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

    View Slide

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

    View Slide

  27. 26
    Sequelize Revision の問題

    View Slide

  28. 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)が定義される

    View Slide

  29. 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)が定義される

    View Slide

  30. 29
    Sequelize Revision 型チャレンジ

    View Slide

  31. 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;
    }

    View Slide

  32. 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;
    }

    View Slide

  33. 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
    }

    View Slide

  34. 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 RevisionCreationAttributesSequelizeRevisionOptions> = 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
    }

    View Slide

  35. 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
    }

    View Slide

  36. 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
    }

    View Slide

  37. 36
    チャレンジ成功…?

    View Slide

  38. 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;

    View Slide

  39. 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;

    View Slide

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

    View Slide

  41. 40
    const 型パラメータの出番

    View Slide

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

    View Slide

  43. 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;

    View Slide

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

    View Slide

  45. 44
    素朴な疑問

    View Slide

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

    View Slide

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

    View Slide

  48. 47
    Thank you

    View Slide