Kozaneba開発日記2021-09-22
Runtypesを使ってJSONが期待した構造であるかのチェック機構を入れていく。
Runtypeと静的な型を同じ名前にするのあまり好きではないなぁ
RTとTをプレフィックスするかな
before
code:ts
export type ItemId = ItemIdBrand & string;
enum ItemIdBrand {
_ = "",
}
after
code:ts
import { Static, String } from "runtypes";
export const RTItemId = String.withBrand("ItemId");
export type TItemId = Static<typeof RTItemId>;
早速例外だ、短いことに積極的に意味がある型はそのままにする
before
code:ts
after
code:ts
import { Number, Static, Tuple } from "runtypes";
export const RT_V2 = Tuple(Number, Number);
export type V2 = Static<typeof RT_V2>;
Brandedな型はRuntypesで書いた方がシンプル
before
code:ts
enum WorldBrand {
_ = "",
}
enum ScreenBrand {
_ = "",
}
export type TScreenCoord = ScreenBrand & V2;
export type TWorldCoord = WorldBrand & V2;
after
code: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
code:ts
export type TKozaneItem = {
type: "kozane";
text: string;
position: TWorldCoord;
id: TItemId;
scale: number;
custom?: {
style?: CSSProperties;
url?: string;
};
};
after:
code: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
code: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
code:ts
export function isTItem(x: any): x is TItem {
return (
isTKozaneItem(x) || isTGroupItem(x) || isTScrapboxItem(x) || isTGyazoItem(x)
);
}
after
code:ts
export function isTItem(x: any): x is TItem {
return (
RTKozaneItem.guard(x) ||
isTGroupItem(x) ||
isTScrapboxItem(x) ||
isTGyazoItem(x)
);
}
他のガードもだんだん起き変わっていって最終的にこのガードも消滅する
他のものも淡々とやっていきましょう
before
code: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
code: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
code:ts
export const RTItem = Union(
RTKozaneItem,
RTGroupItem,
RTScrapboxItem,
RTGyazoItem
);
export type TItem = Static<typeof RTItem>;
isTItemが無事に削除された
あ、CSSPropertiesの問題が見つかったぞ
なるほどCSSPropertiesはinterfaceだから代入できないのか
code: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
もう少しまともに書いておく
code:ts
export const RT_CSSProperties = Unknown.withGuard(
(x): x is CSSProperties => {
return typeof x === "object";
},
{ name: "RT_CSSProperties" }
);
code:test.ts
type T = Static<typeof RT_CSSProperties>;
// T is CSSProperties
RT_CSSProperties.check(1);
// throws ValidationError: Failed constraint check for RT_CSSProperties