TypeScriptでIDなどの特別な数値や文字列を型安全な専用の属性付きプリミティブ型にする

TypeScriptでIDやエンコーディングなど特別なセマンティクスを持つ値にプリミティブ型を維持したまま個別の属性を与えることで型安全な専用の値を作る。言語の型システムをどのように機能させているかについての説明は省略する。

IDは次のように作れる。

namespace Identifier {
  declare class Event<T extends string> {
    private IDENTITY: T;
  }
  export type Number = Event<any> & number;

  export type Id = Event<'Id'> & number;
}

export type EventId = Identifier.Id;

export function makeEventId(id: Identifier.Number): void
export function makeEventId(id: number): EventId
export function makeEventId(id: number): EventId {
  assert(Number.isFinite(id));
  assert(Math.floor(id) === id);
  assert(id >= 0);
  return <EventId>id;
}

IDのように特定のセマンティクス内で識別する値はIDという属性でなくセマンティクスを構成するドメインを主体として型を設計する。ゆえに型はDomain<Attribute>となりAttribute<Domain>とはならない。

エンコーディングは次のように表現できる。

// ./attribute/encode.ts
export declare class Encoded {
  private ENCODE;
}
// ./attribute/normalize.ts
export declare class Normalized {
  private NORMALIZE;
}
// ./domain/url.ts
import { Encoded } from '../attribute/encode';
import { Normalized } from '../attribute/normalize';

namespace Identifier {
  declare class Url<T> {
    private IDENTITY: T;
  }

  export type URL<T> = Url<T> & string;
}

type Url<T> = Identifier.URL<T>;


// https://www.ietf.org/rfc/rfc3986.txt

export type StandardUrl = Url<Normalized & Encoded>;

export function standardizeUrl(url: Url<any>): void
export function standardizeUrl(url: string): StandardUrl
export function standardizeUrl(url: string): StandardUrl {
  return encode(normalize(url));
}


type EncodedUrl = Url<Encoded>;

function encode(url: EncodedUrl): void
function encode<T>(url: Url<T>): Url<T & Encoded>
function encode(url: string): EncodedUrl
function encode(url: string): EncodedUrl {
  return <EncodedUrl>url
    // Trim
    .trim()
    // Percent-encoding
    .replace(/[\uD800-\uDBFF][\uDC00-\uDFFF]?|[\uDC00-\uDFFF]/g, str =>
      str.length === 2
        ? str
        : '')
    .replace(/%(?![0-9A-F]{2})|[^%\[\]]+/ig, encodeURI)
    .replace(/\?[^#]+/, query =>
      '?' +
      query.slice(1)
        .replace(/%[0-9A-F]{2}|[^=&]/ig, str =>
          str.length < 3
            ? encodeURIComponent(str)
            : str))
    .replace(/#.+/, fragment =>
      '#' +
      fragment.slice(1)
        .replace(/%[0-9A-F]{2}|./ig, str =>
          str.length < 3
            ? encodeURIComponent(str)
            : str))
    .replace(/%[0-9A-F]{2}/ig, str => str.toUpperCase());
}


type NormalizedUrl = Url<Normalized>;

function normalize(url: string): NormalizedUrl
function normalize(url: string): NormalizedUrl {
  // Absolute path
  const parser = document.createElement('a');
  parser.href = url || location.href;
  return <NormalizedUrl>parser.href
    // Remove the default port
    .replace(/^([^:/?#]+:\/\/[^/?#]*?):(?:80)?(?=$|[/?#])/, '$1')
    // Fill the root path
    .replace(/^([^:/?#]+:\/\/[^/?#]*)\/?/, '$1/')
    // Use uppercase letters within percent-encoding triplets
    .replace(/%[0-9A-F]{2}/ig, str => str.toUpperCase());
}

Urlとその正規化は公開・一般化されたドメイン横断の知識であるためここではUrlを属性でなくドメインとして定義した。移動距離が長い場合は転送用のオブジェクトで包んでもいいだろう。

これらの手法は以下のライブラリで使われており、具体的な組み込み方と使用方法を確認できる。

github.com github.com

IndexedDBの安全な永続化リソース管理戦略 ~ 有効期限と容量制限

IndexedDBはキャッシュによる通信量削減などに有効だが、クライアント/ユーザー側にサーバー/サービス提供者側から管理・修正不能な状態を作るリスクがあり、最悪の場合クライアント/ユーザー側でキャッシュを削除しない限りサービスが利用不能となる可能性がある。これはサービスの規模および継続性に比例して許容できないリスクとなる。

このためIndexedDBをプロダクションで使うためには開発者から認識されず、または無視される管理外データを自動的に削除する機能が不可欠となる。これは通常、有効期限機能の追加により解決できる。また容量(件数)制限も有効である。有効期限と容量制限は管理外データ、ひいてはこれを管理する問題のサイズに上限を与え、管理可能なサイズに縮小する。

永続データの終末処理はこれまで開発者がすべてのデータを把握して明示的に削除する必要があったが、使わないデータも古いデータも自動的に消えるなら管理は不要となり、終末処理はクッキーと同じようにただ管理を放棄するだけよくなる。このような運用環境では開発者は任意のバージョンのデータだけ考慮すれば済み、それ以外は放置か削除で足りるため永続化リソースの利用コストが大幅に削減される。

このように有効期限と容量制限(超過データの自動削除)の制約を与えることでサーバー管理下にない永続化リソースも相当程度安全かつ低コストで利用できるものとなる。複雑なバージョン管理に由来するバリデーションやマイグレーションの失敗もブラウザの再起動によりすべてのタブのバージョンを最新にそろえればほとんど解決するので、サポートも通常のバグ対応の範疇に収まり容易となる。ここまでくるともはや永続化リソースのために必要な特別な対応は任意のバージョン間でのマイグレーション名前空間の競合の回避程度しかなく、永続化リソースを使用するハードルは非常に低い。

IndexedDBはサーバー管理下にない永続化リソースの制御の難しさから採用しづらいものであったが、有効期限と容量制限の制約を自律させることで採用を検討できる安全性が得られる。以下のライブラリはこの2つの制約を実装したものである。

github.com

CSS in JSで作るStyled Componentsの最小デザイン

CSS in JSでStyled ComponentsをAngularやReactといったフレームワークなしで作る最小デザインを例示する。

次のようなHTMLで表されるDOM ComponentにCSSを適用したいとする。

<article id="id">
  <h1>Title</h1>
  <ul>
    <li>item</li>
    <li>item</li>
  </ul>
</article>

単純にidで絞り込んだCSSを記述したstyle要素があればCSSを不足なく利用できる。

<article id="id">
  <style>#id ul { width: 100px; }</style>
  <h1>Title</h1>
  <ul>
    <li>item</li>
    <li>item</li>
  </ul>
</article>

このHTML(DOM Component)を生成する方法は次のように考えられる。

import TypedHTML from 'typed-dom';

const component = TypedHTML.article({ id: 'id' }, {
  style: TypedHTML.style(`#id ul { width: 100px; }`),
  title: TypedHTML.h1(`title`),
  content: TypedHTML.ul([
    TypedHTML.li(`item`),
    TypedHTML.li(`item`),
  ])
});

idを自動で埋め込めるようにするとミスを防ぐことができ、モジュラリティも得られる。

import TypedHTML from 'typed-dom';

const component = TypedHTML.article({ id: 'id' }, {
  style: TypedHTML.style(`$scope ul { width: 100px; }`),
  title: TypedHTML.h1(`title`),
  content: TypedHTML.ul([
    TypedHTML.li(`item`),
    TypedHTML.li(`item`),
  ])
});

固定idの直接指定は次のように

class Component {
  constructor(private readonly parent: HTMLElement) {
    this.parent.appendChild(this.element);
  }
  private readonly element = TypedHTML.div({ id: 'id' }, [
    TypedHTML.style(`$scope { position: relative; }`)
  ]).element;
  private readonly children = Object.freeze({
    list: new MicroComponent(this.element)
  });
  destroy() {
    this.element.remove();
  }
}

一時idの生成は次のように行える。

class MicroComponent {
  constructor(private readonly parent: HTMLElement) {
    this.parent.appendChild(this.dom.element);
  }
  private readonly dom = TypedHTML.div({ id: `${this.parent.id}-list-${Date.now()}-${Math.random() * 1e9 | 0}` }, {
    style: TypedHTML.style(`$scope ul { width: 100px; }`),
    content: TypedHTML.ul([
      TypedHTML.li(`item`)
    ])
  });
}

以上のようにCSS in JSでStyled Componentsを作るには単にscopeを作る要素を指定するセレクタの埋め込みとCSSを定義するstyle要素の同梱の2機能さえあれば最小構成として十分であり、面倒なフレームワークに抱き合わせられずに簡単に導入できる。

github.com

なおこのライブラリは以下のようにDOM構造の静的型を生成し内部構造を可視化、開発支援するものであり、付属機能としてCSS in JSをサポートしている。

TypedHTMLElement<"article", HTMLElement, {
  style: TypedHTMLElement<"style", HTMLStyleElement, string>;
  title: TypedHTMLElement<"h1", HTMLHeadingElement, string>;
  content: TypedHTMLElement<"ul", HTMLUListElement, TypedHTMLElement<"li", HTMLLIElement, string>[]>;
}>;

Promiseの失敗文脈を使ってはならない理由と組み込み関数では使っていい理由

Promiseのthenメソッドの第二引数およびcatchメソッドのコールバックの呼び出しで表現される失敗文脈、これは原則として使ってはならない。 ネイティブの組み込み関数が使っているからといって真似してPromise値を返し失敗文脈を持つ関数を作ってはならない。

Promiseの失敗文脈には任意の回復可能または予測された失敗と予期せぬ例外を区別できない(例外を強制的に失敗に戻す)欠陥がある。 このためPromiseの失敗文脈の型を定義しようという試みは基本的に無意味だ。 仮に失敗文脈に混入した例外を除去しようとすると以下のようになる。

Promise.resolve()
  .catch(reason => {
    if (reason instanceof Error) throw reason;
    // ...
  });

特定のエラー値を返す場合は以下のようになる。

Promise.resolve()
  .catch(reason => {
    if (reason instanceof Error && reason instanceof DOMError === false) throw reason;
    // ...
  });

とてもバカバカしい。すべての失敗文脈にこの一行が入ったJavaScriptの世界を想像すると頭が痛くなる。 早い話がnullの失敗を繰り返しているのだ。 よってPromiseは原則として失敗を成功文脈で扱い、失敗文脈は例外のみを扱う例外文脈として設計しなければならない。 成否はタプルによる多値で表現し、最初の要素に成否判定のフラグを入れるフォーマットを推奨する。 オブジェクトによる多値は個別に構造を記憶する無駄な労力を要し、生成コストも配列より高いため推奨しない。

new Promise(resolve => resolve([arr.length > 0, arr.pop(), arr]));

一方で組み込み関数が失敗文脈で失敗を表現することは必ずしも間違いではない。 組み込み関数は言語処理系が実装する関数ゆえに絶対に予期せぬ例外を混入させない設計と信頼性確保が可能だからである。 組み込み関数が失敗文脈に例外を含まない(特定のエラー値のみを返す場合を含む)と宣言したらそれは間違いなく含まないとみなせる。 成否表現のフォーマットを強制的に統一させるためといった都合もあるだろう。 同様の意図からライブラリの公開するインターフェイスが失敗文脈の使用を選択する場合もある。 しかしもし失敗と例外が入り混じった失敗文脈を作る関数があればそれはやはり設計が悪い。

対してユーザーの作る失敗文脈付き(Promise値を返す)関数で組み込み関数と同じ信頼性の関数を作ることは容易ではない。 コードサイズが大きくなればなるほどバグにより例外が混入するリスクが増える。 よって失敗文脈を使用する関数を新たに作る場合、その関数は失敗文脈の信頼性を確保するために、低水準で最小単位の関数でなければならない。

単に非同期だからPromiseを使う、Promiseだから失敗を失敗文脈に分離するといった思考論法で作られたPromiseチェインが、要求される失敗文脈の信頼性を自然と備えていることは期待できない。 ゆえに、Promise値を返す組み込み関数を模倣して自身で失敗文脈を持つPromiseチェインや関数を作ることは基本的にアンチパターンである。

async/awaitにおいても同じポリシーが適用でき、async/awaitは原則としてtry-catch文を使用せずに使えるように設計すべきである。 Promiseの失敗文脈を例外文脈として設計した場合、async/awaitのtry-catch文が自然に適合することにも注目してもらいたい。

また、awaitを囲むtryブロックにはawait以外の同期的処理を含めずawaitの失敗文脈の捕捉のみに責務を限定すべきである。 同期処理において捕捉しない例外を非同期処理に限って捕捉しなければならない理由はない。

await sleep(1);
var n = 100;
try {
  await sleep(n);
}
catch (err) {
  throw err;
}

TypeScriptの型検査を無効化して型安全性を失わせる落とし穴5つとその避け方

TypeScriptにはanyを使わないよう注意しても型検査が無効化され型安全性が失われるパターンがいくつかあり、中には致命的なものも存在する。今回はこのパターンをいくつか紹介する。

コールバックのvoid戻り値型

コールバックのvoid戻り値型はすべての型を受け入れるためvoid型 === any型となる。

function f(cb: (a: number) => void): void {
    return cb(0);
}
function g(a: number): number {
    return a; 
}
f(g);

このためvoid型の値はvoid演算子を使用して即座に強制的にvoid型に変換しなければならない。

void f(g);

ここでは手遅れ。

function f(cb: (a: number) => void): void {
    return void cb(0);
}

ここで変換する。

コールバックのパラメータ数

コールバックのパラメータ数の不足は型エラーにならず検出できない。

type F = (cb: (a: number, b: string) => number) => number;
type G = (cb: (a: number) => number) => number;
var f: F = <G>((cb: (a: number) => number) => cb(0));
f((a, b) => a + b.length);

これを回避する方法はない。TypeScript 3.3までに修正済み。

共変・反変

interface A {
  x: string;
}
interface B {
  x: string;
  y: string;
}

const fb = (fa: (a: A) => void) => fa({x: 'x'});
const       fa= (b: B) => alert(b.y);
fb(fa);

まあ、これは仕方ないだろうか。

Generic型

TypeScriptの型検査は基本的に構造型の照合であり、型パラメータは照合の対象となる構造に含まれていないため、構造に反映されない型パラメータの型は無視される。

class C<T extends Foo | Bar> {
    constructor(d) {
        d.cmd();
    }
}
class Foo {
    f;
    cmd() {
        return this;
    }
}
class Bar {
    b;
    cmd() {
        return this;
    }
}
var c: C<Foo> = new C<Bar>(new Bar());

このため型パラメータのみで完結する純粋な幽霊型により型検査を行わせようとしても失敗する。

class C<T> {
}
var c: C<number> = new C<string>();

これを回避するには型パラメータで受け取った型をプロパティやメソッドで持って構造型に反映させなければならない。

class C<T> {
    c: T;
}
var c: C<number> = new C<string>();

このほか、private/protectedアクセス修飾子を使用するとクラスが公称型となり型が一意に識別される(継承クラスの型を除く)。

Generic型定義

TypeScriptがデフォルトで定義しているいくつかの型ではこの性質のため致命的な不整合を見落とす可能性がある。

var s: PromiseLike<number | string> = <PromiseLike<string>>null;
var n: PromiseLike<number> = <PromiseLike<number | string>>s;

どれだけ厳格に型を守っても、上記のように数値のみを返す型が文字列を返すようになる不正な代入が可能である。 型によっては不整合を検出するので不正に代入可能な型を即座に見分けることは難しい。

var s: ArrayLike<number | string> = <ArrayLike<string>>null;
var n: ArrayLike<number> = <ArrayLike<number | string>>s;

この不整合を防ぐにはインターフェイスを拡張して先ほどと同様型パラメータを構造に落とせばよい。

interface PromiseLike<T> {
    _?: T;
}

import/export文を含めると新規の型定義となるため含ないこと。 拡張を行っても外部からは拡張を含まない.d.tsファイルだけを参照させれば拡張を隠蔽できるので外部の型定義を汚染したり競合したりすることはない。

TypeScriptではこの種の不整合によるバグを防ぐため上記のような宣言をおまじないのように毎回追加しておく必要があるだろう。TypeScript 3.3までに修正済み。

まとめ

型があってるのに変な値が入るバグに苦しみたくなかったらここに書いたパターンと対処法を開発メンバーのTypeScript使用者全員に周知することをお勧めする。

TypeScript1.8以降で追加予定または協議中の注目の新機能12つ

TypeScript1.8以降で追加予定または協議中の注目の新機能を紹介します。

この記事はTypeScript アドベントカレンダー2015 25日目の記事です。

戻り値型の厳格な検査(--noImplicitReturns) [Merged]

github.com

github.com

戻り値が定義した型以外になることを禁止します。 必ずオンにしましょう。しない理由がありません。 この調子でFlowtypeと型安全性をガンガン競ってほしいところです。

// The followings are all wrong in a typed context yet the compiler ignores them as a valid code:
function SomeMethod(): number {
    return;
}

function SomeMethod(): number {
    return null;
}

function SomeMethod(): number {
    if (false)
        return 1;
}

function SomeMethod(): number {
    try {
        return 0;
    }
    catch (ex) {
    }
}

Nullable型の禁止(--noImplicitNull) [Suggestion]

github.com

nullやvoidがanyやvoid以外の型に混入しないよう暗黙的なキャストを禁止します。 変数ならconstを使う方法もあるのですがオブジェクトやパラメーターはそうはいきません。 --noImplicitNullオプションでの制御を支持しています。 バグのない世界のために今すぐ+1

let n: number = null; // error
let m: number = undefined; // error

タイプガードへのコントロールフロー解析の適用 [Discussion]

github.com

タイプガードはこれまで条件分岐先のブロックか式にしか適用されませんでしたがタイプガードの結果が以降のすべてのコードに適用されるようになるのでガード節を型解決に使えるようになります。 まだ協議中なのでぜひ+1してきてください。

function foo(x: number | string) {
    if (typeof x === 'string') {
        return;
    }
    // x should now be a number
    if (2 % x !== 0) {
        // do something
    }
}

String Literal Types [Merged]

github.com

変数や関数に与えられる文字列の値を制限できるようになります。 ありがてぇ。

type CardinalDirection = "North"
                       | "East"
                       | "South"
                       | "West";

function move(distance: number, direction: CardinalDirection) {
    // ...
}

let a: "foo" | number = "foo"; // valid
let b: "bar" | "baz" = "foo";  // invalid

Readonly(final)アクセス修飾子 [Scheduled]

github.com

プロパティの再代入を型として禁止し、不変オブジェクトの定義を可能にします。 待ち望まれた機能です。 個人的にはfinalのほうが好きです。

interface Point {
    x: number;
    y: number;
}
interface ImmutablePoint {
    readonly x: number;
    readonly y: number;
}
var pt: ImmutablePoint = { x: 4, y: 5 }; // OK, can convert mutable to non-mutable
pt.x = 5; // Error, 'pt.x' is not a valid target of assignment

var pt2: Point = pt; // Error, cannot convert readonly 'x' to mutable 'x'

// Possibly bad behavior
var pt3: Point = { x: 1, y: 1 };
var pt4: ImmutablePoint = pt3; // OK
pt3.x = 5; // pt4.x is also changed?

// Really bad behavior
/** This function was written in TypeScript 1.0 **/
function magnitudeSquared(v: { x: number; y: number }) {
   return v.x * v.x + v.y * v.y;
}
// Now try to use it with ImmutablePoint
console.log(magnitudeSquared(pt)); // Error, cannot use readonly object in non-readonly call

object型の追加 [Accepted]

github.com

今まで地味にオブジェクト型で制約する型がありませんでしたがこれでようやく制約できるようになります。

出力したJSモジュールファイルの結合 [Merged]

github.com

出力したJSモジュールファイルを1つのファイルに結合します。

Browserifyがいらなくなくなります。 単にオプションを使って結合しただけではrequirejsに依存しますが、以下のスニペットをヘッダとして追加することでブラウザでもNodeでも動作するIsomorphicなライブラリとなります。

gist.github.com

こちらのライブラリで実際にこの機能を使ってBrowserifyなしで同じ相互運用性を実現していますので試してみたい方は参考にするとよいでしょう。

github.com

循環参照が許されませんが、なんくるないさという気持ちになれば問題ありません。

ファイルパスのglobによるパターンマッチ [Scheduled]

github.com

ファイルパスにワイルドカードなどのglobのパターンマッチを使えるようにします。 コンパイルするファイルを何十個も列挙してメンテまでするのはつらすぎます。 v2.xで実装が予定されています。

Union/Intersection typesの型推論強化 [Merged]

github.com

Union/Intersection typeの分解時の型推論が強化され再利用性が向上します。 とてもうれしい。

type Maybe<T> = T | void;

function isDefined<T>(x: Maybe<T>): x is T {
    return x !== undefined && x !== null;
}

function isUndefined<T>(x: Maybe<T>): x is void {
    return x === undefined || x === null;
}

function getOrElse<T>(x: Maybe<T>, defaultValue: T): T {
    return isDefined(x) ? x : defaultValue;
}

function test1(x: Maybe<string>) {
    let x1 = getOrElse(x, "Undefined");         // string
    let x2 = isDefined(x) ? x : "Undefined";    // string
    let x3 = isUndefined(x) ? "Undefined" : x;  // string
}

function test2(x: Maybe<number>) {
    let x1 = getOrElse(x, -1);         // number
    let x2 = isDefined(x) ? x : -1;    // number
    let x3 = isUndefined(x) ? -1 : x;  // number
}

thisの型定義と型推論 [Discussion]

github.com

thisの型定義は避けては通れないのではないでしょうか。 クラスの自己型としてのthisはTS1.7のポリモーフィックthisで実装済みです。

let f = function(this: {data: number}) {
  console.log(this.data2) 
}
let o = {
  data: 12
  f: f
  g: function() {   // this is inferred from the contextual type
    console.log(this.data); 
  }
}
function f(this: {n: number}, m: number) {
    return this.n + m;
}
class C {
  n: number
  m1(this:this, m: number) {
    return this.n + m
  }
  m2(this: {n: number}, m: number) {
    return this.n + m
  }
}

型パラメーターによる制約 [Merged]

github.com

型パラメーターで型パラメーターの制約を作れるようになります。 今まで何度この型を書けずに涙を飲んだかわかりません。

function assign<T extends U, U>(target: T, source: U): T {
    for (let id in source) {
        target[id] = source[id];
    }
    return target;
}

let x = { a: 1, b: 2, c: 3, d: 4 };
assign(x, { b: 10, d: 20 });
assign(x, { e: 0 });  // Error

?プロパティーアクセサ [Suggestion]

github.com

CoffeeScriptの便利なやつです。 ESの仕様に入ってほしいくらいです。 ただ最初に提案されたものはx?.y?.zの戻り値の型がx|y|zであまりよろしくありません。

var x = { y: { z: null, q: undefined } };
console.log(x?.y?.z?.foo); // Should print 'null'
console.log(x?.baz); // Still an error
console.log(x.y.q?.bar); // Should print 'undefined'

ミスタイプ(ポスト)はデザイン [Design]

github.com

マイクロソフトによると指がすべって立てられたIssueはデザインによるものだそうです。

草。

まとめ

TypeScriptはバージョンアップを重ねるたびにますます洗練されていき、来年は上述の機能の追加によりさらに躍進が期待されます。

2016年もさらにヒートアップするTypeScriptをよろしく。