generated at
Kozaneba開発日記2021-09-22
Runtypesを使ってJSONが期待した構造であるかのチェック機構を入れていく。

Runtypeと静的な型を同じ名前にするのあまり好きではないなぁ
RTとTをプレフィックスするかな
before
ts
export type ItemId = ItemIdBrand & string; enum ItemIdBrand { _ = "", }
after
ts
import { Static, String } from "runtypes"; export const RTItemId = String.withBrand("ItemId"); export type TItemId = Static<typeof RTItemId>;

早速例外だ、短いことに積極的に意味がある型はそのままにする
before
ts
export type V2 = [number, number];
after
ts
import { Number, Static, Tuple } from "runtypes"; export const RT_V2 = Tuple(Number, Number); export type V2 = Static<typeof RT_V2>;

Brandedな型はRuntypesで書いた方がシンプル
before
ts
enum WorldBrand { _ = "", } enum ScreenBrand { _ = "", } export type TScreenCoord = ScreenBrand & V2; export type TWorldCoord = WorldBrand & V2;
after
ts
export const RTScreenCoord = RT_V2.withBrand("Screen"); export type TScreenCoord = Static<typeof RTScreenCoord>; export const RTWorldCoord = RT_V2.withBrand("World"); export type TWorldCoord = Static<typeof RTWorldCoord>;

さて、準備が整ったので大物をやっつける
before
ts
export type TKozaneItem = { type: "kozane"; text: string; position: TWorldCoord; id: TItemId; scale: number; custom?: { style?: CSSProperties; url?: string; }; };
after:
ts
const RTKozaneItem = Record({ type: Literal("kozane"), text: String, position: RTWorldCoord, id: RTItemId, scale: Number, custom: Optional( Record({ style: Dictionary(String.Or(Number)).optional(), url: String.optional(), }) ), }); export type TKozaneItem = Static<typeof RTKozaneItem>;
CSSPropertiesみたいなサードパーティの型をどうするかは悩ましい問題
のように静的な型定義からコード生成しちゃうというアプローチはあるが、あんまりやりたくない
とりあえずDictionaryにして、将来的にどんな問題が起きるのかを観察しよう

JSONが期待した構造であるかのチェックのために書いてたコード
before
ts
export function isTKozaneItem(x: any): x is TKozaneItem { return ( x.type === "kozane" && typeof x.text === "string" && isV2(x.position) && typeof x.id === "string" && typeof x.scale === "number" ); }
after: 単純に削除してよい、自前で実装する必要がない
使ってるところも修正
before
ts
export function isTItem(x: any): x is TItem { return ( isTKozaneItem(x) || isTGroupItem(x) || isTScrapboxItem(x) || isTGyazoItem(x) ); }
after
ts
export function isTItem(x: any): x is TItem { return ( RTKozaneItem.guard(x) || isTGroupItem(x) || isTScrapboxItem(x) || isTGyazoItem(x) ); }
他のガードもだんだん起き変わっていって最終的にこのガードも消滅する

他のものも淡々とやっていきましょう
before
ts
export type TGroupItem = { type: "group"; text: string; position: TWorldCoord; items: TItemId[]; id: TItemId; scale: number; // scale of Nameplate Kozane isOpen: boolean; custom?: { style?: CSSProperties }; };
after
ts
export const RTGroupItem = Record({ type: Literal("group"), text: String, position: RTWorldCoord, items: Array(RTItemId), id: RTItemId, scale: Number, isOpen: Boolean, custom: Record({ style: RT_CSSProperties.optional(), }).optional(), }); export type TGroupItem = Static<typeof RTGroupItem>;
CSSPropertiesがまた出てきたので将来置き換える時に備えて名前をつけておく
export const RT_CSSProperties = Dictionary(String.Or(Number));

before
export type TItem = TKozaneItem | TGroupItem | TGyazoItem | TScrapboxItem;
after
ts
export const RTItem = Union( RTKozaneItem, RTGroupItem, RTScrapboxItem, RTGyazoItem ); export type TItem = Static<typeof RTItem>;
isTItemが無事に削除された

あ、CSSPropertiesの問題が見つかったぞ
なるほどCSSPropertiesはinterfaceだから代入できないのか
ts
const f = (x: CSSProperties) => { type T = Static<typeof RT_CSSProperties>; let y: T = x; // Type 'CSSProperties' is not assignable to type '{ [x: string]: string | number; [x: number]: string | number; [x: symbol]: string | number; }'. // Index signature for type 'string' is missing in type 'Properties<string | number, string & {}>'. };

どうしたらいいのか調べたらinterface Runtype にwithGuardってmethodがあった
型ガードを指定できる
その型ガードがis Tなら静的型に変換した時もTになる
から既存のサードパーティ静的型を別のものに変更する変更する必要はない
どれくらいチェックするかは型ガードをどれくらい真剣に書くかでコントロールできる。
一番雑なやつ
Unknown.withGuard((x): x is CSSProperties => true)
「なんか知らんけど、これはCSSPropertietのはず!」
まあ、雑だけど、今もチェックしてないから悪くはならないw
もう少しまともに書いておく
ts
export const RT_CSSProperties = Unknown.withGuard( (x): x is CSSProperties => { return typeof x === "object"; }, { name: "RT_CSSProperties" } );
test.ts
type T = Static<typeof RT_CSSProperties>; // T is CSSProperties RT_CSSProperties.check(1); // throws `ValidationError: Failed constraint check for RT_CSSProperties`