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