Skip to content

Instantly share code, notes, and snippets.

@mizchi
Last active July 14, 2022 11:50
Show Gist options
  • Save mizchi/1b5c45b50ca0df3e78d9f7697c336ecc to your computer and use it in GitHub Desktop.
Save mizchi/1b5c45b50ca0df3e78d9f7697c336ecc to your computer and use it in GitHub Desktop.

非破壊 TypeSctript

mizchi / TypeScript Meetup 2


About Me

  • mizchi / 竹馬光太郎
  • フロントエンドと Node.js
  • あとはググって

はじめに

  • Modern JS ≒ TypeScript の時代になった
  • なので型を書け

この発表の目的

  • TypeScript を導入しない言い訳を全部潰す
  • そのために痛みがない導入・運用を提示する

Outline

  • TS の型アノテーションとはなにか?
  • 導入編
  • 発展編
  • アンチパターン

型アノテーションとはなにか?


TS の型アノテーションとは何「ではない」か

  • メモリ確保量を決めるもの、ではない
    • TS の型はインターフェースしか知らない
  • 実行時の挙動を決めるもの、ではない
    • TS の型宣言はランタイムに関与しない

// TS は ArrayBuffer であることを知っているが
// VM で確保されるメモリに興味がない
const buf: ArrayBuffer = new ArrayBuffer(8);

それでも、なぜ型アノテーションを書くのか

  • 人間のためのインターフェース宣言
  • 実行可能な Lint
  • それらによくコード品質の向上・レビューコストの削減

TypeScript はコンパイラというより、「型を検証可能な Lint ツール


TypeScript Compiler(tsc) の役割

主機能

  • 型の静的検査
  • エディタへの情報提供 - Language Server Protocol

おまけ機能 (babel で代替可能)

  • 型アノテーションの除去
  • ES2015 => ES5

これまでの(歴史の)あらすじ!

「Java の静的型付けが大変で、反動で動的なのが流行ったけど、 推論あればそうでもなかった。むしろドキュメントとして有用。 でも動的が流行ったあとだから、一旦は漸進的型付けで行く」


導入編


TS 導入最小ステップを考える


コンパイラとして使う

$ npm install typescript webpack webpack-cli ts-loader --save-dev

最小 tsconfig.json

{
  "compilerOptions": {
    "target": "es5",
    "module": "commonjs",
    "esModuleInterop": true
  }
}

最小 webpack.config.js

module.exports = {
  resolve: {
    extensions: [".ts"]
  },
  module: {
    rules: [
      {
        test: /\.ts$/,
        use: [
          {
            loader: "ts-loader",
            options: {
              transpileOnly: true // 型チェックしない!!!
            }
          }
        ]
      }
    ]
  }
};

最小 TS 用 Hello World

src/index.ts

// 型はわざと間違ってる。アノテーションが取り除かれることを確認する
const text: number = "World";
console.log(`Hello, ${text}`);

最初に覚えるコマンド

$ npx webpack # build only
# => dist/main.js
$ npx tsc -p . --noEmit # type check only

エラーを確認

src/index.ts:1:7 - error TS2322: Type '"World"' is not assignable to type 'number'.

1 const text: number = "World";

最初にやること

  • 「コードを修正せずに」自分の .js.ts にする
  • 「CI で型違反を検査せずに」ふるまいを手動/ユニットテストで確認

なぜこうなるか

  • 初手はコンパイラとしての機能を保証する
    • どうせ最初は型チェックを通せない
  • 後々効く: コンパイルと型チェックの分離で高速化
    • どうせ後でも IDE で型違反を見る
    • どうせ後でも CI で型チェックする

(awesome-typescript-loader やめとけ)


ライブラリ型定義をいれる

$ npm install --save-dev @types/<pkg-you-want>

自分がほしいやつを一通り叩いてみる (一部のライブラリは TS 型定義を同梱)

動作確認

import { range } from "lodash";
range(3);

ライブラリ型定義を潰す

たぶん全部の型定義ファイルは揃わないので一旦潰す

  • src/decls.d.ts
declare module "xxx"; // xxx がない or 合わせるのがきつい
// ちょっと頑張る場合
declare module "yyy" {
  export function foo(input: string): number;
}

any 祭り

  • tsc -p . --noEmit で落ちた場所を修正していく
  • 読み下しながら自明な範囲で numberstring を付与する
  • わからなかったら any@ts-ignore で潰す
  • ロジックを変更しない!!!!

潰されるコード

function genMagicId(): number | string {
  // @ts-ignore
  return super_magical_func();
}
  • 本当に守りたいのは関数の入出力
  • 暗黒面で心折れるぐらいなら any で全部潰す

慣用句: as any as ...

const foo: Foo = (foobar as any) as Foo;
  • 推論過程が導けない場合に仕方なく書くもの
  • 多用厳禁

CI を通す

  • 雑に修正したら CI でテストを流す

.circleci/config.yml

    steps:
      ...
      - run: npx tsc -p . --noEmit

circleci ない人は husky などで頑張って


結果: ガバガバ状態で導入完了


発展編


がんばる tsconfig.json

{
  "compilerOptions": {
    "target": "es5",
    "module": "es2015",
    "esModuleInterop": true,
    // 段階的に有効化
    "strict": false, // "use strict" 有効化
    "strictNullChecks": false, // null|undefined 厳格化
    "noImplicitAny": false // 推論不可能なときにアノテーション必須に
  }
}

↓ ほど難易度高い


大事なこと(これだけ覚えて帰って!)

  • 型とロジックを同時に修正しない
    • 推論などで発見すると嬉しくて修正したくなる
      • => 確認の工数がかかる
      • => マージされない
      • => 治安悪い状態が続く
  • 別 Issue にしましょう

型のコスパ感覚

  • 型付けるとコスパ良い順
    • ORM 周り (node.js)
    • API レスポンスの返り値
    • モデル層(redux/vuex)
    • View の入力(React/Vue Props)
    • View ステート層(React state / Vue data)

ドメイン層を守る

  • 本当に守りたいのは(たぶん)データベースと API
  • スキーマ定義から型定義生成ツールがあると良い
    • grpc => .d.ts
    • graphql => .d.ts
    • jsonschema => .d.ts
    • 自作

キャストによる治安悪化

const id: string = number_or_string as string;
  • 本当に型の契約が守れてるかが自己責任
  • 新規にコードを書く際は Type Refinements (if /switch による union type 絞り込み) を優先

アンチパターン集


typeof initialState

const initialState = {...};
export type State = typeof initialState;
  • 型とインスタンスの従属関係が逆
  • 本当に管理したいのは潜在的に State のとりうる状態

型引数多すぎ問題

export class StateManager<A, B, C, D, E, F> {...}
  • 人間が管理できる型引数はたぶん 2 つぐらいまで
  • ライブラリ作者は 3 つ、アプリケーション内なら 2 つまでが感覚的なセーフライン

潰れる Action

export type Action = {
  type: string;
  payload: any;
};
  • 間違っちゃいないんだけど union type でもっと詳細に書ける

namespace と import foo = require(...)

import module = require("module");
  • ESModules 導入以前の独自モジュールシステム。新規で書く場合には不要

議論用のテーマ: どう思いますか?

inline


参考


おわり

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment