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を属性でなくドメインとして定義した。移動距離が長い場合は転送用のオブジェクトで包んでもいいだろう。
これらの手法は以下のライブラリで使われており、具体的な組み込み方と使用方法を確認できる。