Skip to content

Instantly share code, notes, and snippets.

@mizchi
Last active August 19, 2023 14:09
Show Gist options
  • Star 104 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mizchi/9e71569f72187af749adfecea49fb38a to your computer and use it in GitHub Desktop.
Save mizchi/9e71569f72187af749adfecea49fb38a to your computer and use it in GitHub Desktop.

非破壊 TypeSctript

mizchi / TypeScript Meetup 2


About

  • mizchi / 竹馬光太郎
  • フロントエンドと Node.js
  • 株式会社プレイド 2019/7~
    • Frontend Ops 周り
    • DOM 差分監視して色々

はじめに

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

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

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


この発表の目的

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

Outline

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

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

型は JIT に優しい

  • V8 等の JIT(実行時最適化)は、何度も実行される処理のデータのシェイプ(≒ 型)を仮定する
  • 仮定が崩れると速度が落ちる(deopt)
  • 厳密には色々あるが、綺麗な型がつく方が高速な傾向

導入編


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


コンパイラとして使う

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

最小 tsconfig.json

{
  "compilerOptions": {
    "target": "es5",
    "module": "es2015", // ESM は webpack が変形する
    "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 型定義を同梱)

// npm install --save @types/lodash
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;
  • 推論過程が導けない場合に仕方なく書くもの
  • 多用厳禁

中身を無視して型定義

  • src/foo.js # 中は気にしないことにする
  • src/foo.d.ts # 外から型を指定する
// foo.d.ts
export const hoge: <T>(t: T) => Promise<T>;

CI を通す

  • パスしたら CI で型チェックを流す

.circleci/config.yml

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

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


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


発展編


がんばる tsconfig.json

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

↓ ほど難易度高い


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

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

型のコスパ感覚

  • 型付けるとコスパ良い順
    • ORM 周り (node.js)
    • API レスポンスの返り値や、それを使う周辺
    • Model/Store 層(redux/vuex)
    • View の入力(React/Vue Props)
    • View ステート層(React state / Vue data)

テストのコスパ感覚

  • 型アノテーション増やす > 単体テスト
    • テスト書かないでいいわけではないが型書くのを優先
    • 単体テストで型の効果を補強するイメージ
    • そもそもフロントエンドのテストはやりづらい(ので静的解析優先)

ドメイン層を守る

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

とにかく静的解析を強化

  • @typescripc-eslint
    • 色々やったが @typescripc-eslint/no-ununsed-vars が一番効く
  • prettier
  • jest のカバレッジ機能 + React SSR でスナップショットなど

TS 筋を鍛える

  • DeepDive https://typescript-jp.gitbook.io/deep-dive/
    • Generics, Union Type, Promise 表現
  • 自作ライブラリの .d.ts 書く
  • フレームワークごとのイディオム
  • 個人的に Scala の経験が生きた

アンチパターン集


typeof initialState

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

型引数多すぎ問題

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

難しい型を書いてしまう

  • 例: react-redux@connect のヤバさ
  • ライブラリ作者は抽象度が必要 <=> アプリケーション内で難しい型は割れ窓
    • 同僚に any にされる
    • any になる可能性があるものは、 any になる

潰れる Action

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

namespace と import foo = require(...)

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

キャストによる治安悪化

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

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

inline


参考


おわり

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