勉強日記

チラ裏

Programming TypeScript ch11 Interoperating with JavaScript

www.oreilly.com


  • 型のない海のなかでTypeScriptの島を作るところから始める

Type Declarations

  • .d.tsファイル
  • もともと型のないJavaScriptコードに型情報を付与するもの
  • 通常のTypeScriptとだいたい同じ。異なるところ:
    • 値を含まない
      • デフォルト値なども含まない
    • 値を含めてはならないが、「値がどこかに存在すること」は宣言できる
      • declare
      • 【補】Cのexternalみたいな
    • 利用者から見える型情報のみを記述する
      • 関数内部の局所変数の型情報などはexportしない
  • 例: 以前作ったMaybeクラス

Maybe.ts

type NotNullOrUndefined = {}

class Just<T extends NotNullOrUndefined>{
  constructor(private value: T) { }

  flatMap(f: (value: T) => Nothing): Nothing
  flatMap<U extends NotNullOrUndefined>(f: (value: T) => Just<U>): Just<U>
  flatMap<U>(f: (value: T) => Maybe<U>): Maybe<U>
  {
    return f(this.value)
  }

  getOrElse(_: T): T {
    return this.value
  }
}

class Nothing {
  flatMap(_: (value: never) => Nothing): Nothing
  flatMap<U extends NotNullOrUndefined>(_: (value: never) => Just<U>): Nothing
  flatMap<U>(_: (value: never) => Maybe<U>): Nothing
  flatMap<U>(_: (value: never) => Nothing|Just<U>|Maybe<U>): Nothing
  {
    return this
  }

  getOrElse<T extends NotNullOrUndefined>(value: T): T {
    return value
  }
}
type Maybe<T> = T extends NotNullOrUndefined ? Just<T> : Nothing

function Maybe<T extends NotNullOrUndefined>(value: T): Just<T>
function Maybe(value: null | undefined): Nothing
function Maybe<T>(value: T): Maybe<T>
function Maybe<T>(value: T): Just<T>|Nothing|Maybe<T>
{
  if (value == null) {
    return new Nothing
  }

  return new Just(value)
}
  • d.tsファイル生成
tsc -d src/Maybe.ts 
declare type NotNullOrUndefined = {};
declare class Just<T extends NotNullOrUndefined> {
    private value;
    constructor(value: T);
    flatMap(f: (value: T) => Nothing): Nothing;
    flatMap<U extends NotNullOrUndefined>(f: (value: T) => Just<U>): Just<U>;
    getOrElse(_: T): T;
}
declare class Nothing {
    flatMap(_: (value: never) => Nothing): Nothing;
    flatMap<U extends NotNullOrUndefined>(_: (value: never) => Just<U>): Nothing;
    flatMap<U>(_: (value: never) => Maybe<U>): Nothing;
    getOrElse<T extends NotNullOrUndefined>(value: T): T;
}
declare type Maybe<T> = T extends NotNullOrUndefined ? Just<T> : Nothing;
declare function Maybe<T extends NotNullOrUndefined>(value: T): Just<T>;
declare function Maybe(value: null | undefined): Nothing;
declare function Maybe<T>(value: T): Maybe<T>;
  • TSで書かれたライブラリ自身にとってd.tsは無用の長物
    • 元のTS実装自体の型情報にアクセスできるので
  • 他のライブラリやクライアントコードからアクセスする時にd.tsは有用
  • TS/JS両対応のライブラリをバッケージングする際にも有用
    • 考えられるパッケージング方法は2つ
      • TypeScriptファイルと、コンパイルJavaScriptファイル両方をパッケージングする
      • d.tsファイルと、コンパイルJavaScriptファイルを別々にパッケージングする
        • npm install --save-dev @types/xxxってやつ
    • 後者の利点
  • 値を含まない型宣言のことを"ambient"と言って区別することがある
  • 型定義ファイルはscript mode
    • importしなくていい

Ambient Variable Declarations

process = {  // Error: Cannot find name 'myProcess'. Did you mean 'process'?
  env: {
    NODE_ENV: 'production'
  }
}

グローバルオブジェクトを拡張したいときに必要:

declare let myProcess: {
  env: {
    NODE_ENV: 'production'|'development'
  }
}

myProcess = {
  env: {
    NODE_ENV: 'production'
  }
}
  • tsconfigのlibフィールドは各種d.tsファイルの読み込み設定
    • domとかwebworkerとか

Ambient Type Declarations

  • 明示的にimportしなくてよくなる

Ambient Module Declarations

module-name.ts

export let myExport = 3.14
let myDefaultExport = {a: 'pi'}
export default myDefaultExport

types.d.ts

declare module 'module-name' {
  export type MyType = number
  export type MyDefaultType = {a: string}
  export let myExport: MyType
  let myDefaultExport: MyDefaultType
  export default myDefaultExport
}

index.ts

import ModuleName, {myExport} from './module-name'

ModuleName.a       // string
const b = myExport // number
  • モジュールがネストしている場合は declare module '@most/core'のようにする
  • ワイルドカード利用可能

Gradually Migrating from JavaScript to Typescript

  • TSは元来JSとの相互運用を念頭に開発された言語
  • 痛みは伴うが、JSから漸進的にTSに移行できる

Step 1: Add TSC

tsconfig.json

{
  "compilerOptions": {
    "allowJs": true,
    ...
  • .jsファイルもtscを通すようになる
    • 型チェックは行われない
    • トランスパイルは行う

Step 2a: Enable Typechecking for JavaScript (Optional)

class A {
  x = 0 // number
  method() {
    this.x = 'foo'
  }
  otherMethod() {
    this.x = ['array', 'of', 'strings']
  }
}
  • tsconfigのcheckJsを有効にする
{
  "compilerOptions": {
    "allowJs": true,
    "checkJs": true,
...
  • エラーを出してくれるようになる
class A {
  x = 0 // number
  method() {
    this.x = 'foo' // Error: Type '"foo"' is not assignable to type 'number'.
  }
  otherMethod() {
    this.x = ['array', 'of', 'strings'] // Error: Type 'string[]' is not assignable to type 'number'.
  }
}
  • 既存のコードベースが巨大でエラーが出すぎる場合は、tsconfigのものは無効化し、@ts-checkアノテーションを用いてファイル単位でチェックする
// @ts-check

class A {
  x = 0 // number
  method() {
    this.x = 'foo' // Error: Type '"foo"' is not assignable to type 'number'.
  }
  otherMethod() {
    this.x = ['array', 'of', 'strings'] // Error: Type 'string[]' is not assignable to type 'number'.
  }
}

Step 2b: Add JSDoc Annotations (Optional)

// @ts-check

class A {
  /**
   * @type {number|string|string[]} x
   */
  x = 0 // number
  method() {
    this.x = 'foo'
  }
  otherMethod() {
    this.x = ['array', 'of', 'strings']
  }
}
  • 関数なら@param@returnsが使える
    • 【補】PHPでいつもやってるアレ

Step 3: Rename Your Files to .ts

index.ts

class A {
  x: number|string|string[] = 0
  method() {
    this.x = 'foo'
  }
  otherMethod() {
    this.x = ['array', 'of', 'strings']
  }
}

tsc出力結果

dist/index.js

"use strict";
class A {
    constructor() {
        this.x = 0;
    }
    method() {
        this.x = 'foo';
    }
    otherMethod() {
        this.x = ['array', 'of', 'strings'];
    }
}
//# sourceMappingURL=index.js.map

Step 4: Make It strict

  • あらかた.jsファイルを.tsファイルに置き換えることができたら
{
  "compilerOptions": {
    "allowJs": false,
    "checkJs": false,
...
  • JSとの相互運用フラグを無効化し、適宜厳しい設定にしていく

  • 完全に管理下におけるJSコードベースに型情報を付与していく営みは以上
  • 管理下におけない場合 -- 例えばnpmからインストールしたパッケージの場合は?

Type Lookup for JavaScript

  • TSからJSをimportするときの規則

ローカルファイル

  1. importする.jsファイルと同階層の.d.tsファイルを探す
  2. なければ、tsconfigのallowJscheckJsフラグがtrueならば、JSファイルの型推論を行う(JSDoc併用)
  3. これもなければ、全てをanyとして扱う

サードパーティ

  1. ローカルの型定義(ambient module declarations)を探す。あれば、それを使う
  2. なければ、package.jsonを参照し、typestypingsフィールドがあれば、指定の型定義を見に行く
  3. なければ、node_modules/@types/ディレクトリを走査し、対応する型定義を探す
    • npm i reactに対応するのは npm i --save-dev @types/react
  4. それもなければ、ローカルファイルの1-3を試みる

column: TSC Settings: types and typeRoots

  • node_modules/@types以外を走査するようにできる

Using Third-Party JavaScript

  • いくつかのパターンがある

JavaScript That Comes with Type Declarations

  • 例: npm i rxjs
ls -l node_modules/rxjs | head -7
total 332
-rw-rw-r--  1 wand wand    42 Oct 26  1985 AsyncSubject.d.ts
-rw-rw-r--  1 wand wand   261 Oct 26  1985 AsyncSubject.js
-rw-rw-r--  1 wand wand   114 Oct 26  1985 AsyncSubject.js.map
-rw-rw-r--  1 wand wand    45 Oct 26  1985 BehaviorSubject.d.ts
-rw-rw-r--  1 wand wand   267 Oct 26  1985 BehaviorSubject.js
-rw-rw-r--  1 wand wand   120 Oct 26  1985 BehaviorSubject.js.map

JavaScript That Has Type Declarations on DefinitelyTyped

github.com

  • @typesがコミュニティにより保守されている
    • 不完全だったり不正確だったりすることも。注意
  • 例: npm i @types/lodash --save-dev
ls -l node_modules/@types/lodash/ | head -n 7
total 1624
-rw-rw-r-- 1 wand wand   1141 May 15 19:01 LICENSE
-rw-rw-r-- 1 wand wand    838 May 15 19:01 README.md
-rw-rw-r-- 1 wand wand     45 May 15 19:01 add.d.ts
-rw-rw-r-- 1 wand wand     49 May 15 19:01 after.d.ts
-rw-rw-r-- 1 wand wand     45 May 15 19:01 ary.d.ts
-rw-rw-r-- 1 wand wand     51 May 15 19:01 assign.d.ts

JavaScript That Doesn't Have Type Declarations on DefinitelyTyped

  • そうそうないケース
  • 自前のambient module declarationsを作る
  • 作ったらコントリビュートしよう
    • モジュール開発元に直接コントリビュートする
    • DefinitelyTypedリポジトリにコントリビュートする

英語

  • leeway
    • 余裕、ゆとり
  • sleuth
    • 探偵
  • detour
    • 迂回