å ¬éæ¥: 2022-02-26
æŽæ°æ¥: 2022-02-26
Zennèšäºã¯ãã¡ãã
typescannerãšããObjectã®åã¬ãŒãé¢æ°ãçŽæçã«å®çŸ©ã§ããã©ã€ãã©ãªãäœããŸããã
詳ãã䜿ãæ¹ã¯README.mdã«æžããã®ã§ããã®èšäºã§ã¯ç°¡åãªçŽ¹ä»ãããŠããããšæããŸãã
https://github.com/yona3/typescanner
æåã«åã¬ãŒãã®èª¬æãæžããŠãããå°ãé·ããªã£ãŠããŸã£ãã®ã§ãå ã«README.mdã®ExampleãèŒããŠãããŸãã(ãåç¥ã®æ¹ã¯åã¬ãŒãã®èª¬æéšåãèªã¿é£ã°ããŠããã ããŠãåé¡ãããŸããã)
// define the union type
const Lang = {
ja: "ja",
en: "en",
} as const;
type Lang = typeof Lang[keyof typeof Lang]; // "ja" | "en"
const langList = Object.values(Lang);
type Post = {
id: number;
author: string | null;
body: string;
lang: Lang;
isPublic: boolean;
createdAt: Date;
tags?: string[] | null;
};
// create a scanner
const isPost = scanner<Post>({
id: number,
author: union(string, Null),
body: string,
lang: list(langList),
isPublic: boolean,
createdAt: date,
tags: optional(array(string), Null),
});
const data = {
id: 1,
author: "taro",
body: "Hello!",
lang: "ja",
isPublic: true,
createdAt: new Date(),
tags: ["tag1", "tag2"],
} as unknown;
// scan
const post = scan(data, isPost);
post.body; // OK
TypeScriptã䜿ã£ãŠãããšany
åãunknown
åãunion
åãªã©ãå®éã®å€ã®åãäžæãªå€æ°ãæ±ãå Žé¢ãããåºãŠããŸãããã®äžã§ãfetch APIãªã©ã§APIããããŒã¿ãååŸããéã¯ãè€æ°ã®ããããã£ãæã£ããªããžã§ã¯ããæ±ãããšã«ãªããŸããfetch APIã®å ŽåãååŸããããŒã¿ã®å€ã¯any
åã«ãªããŸãããžã§ããªã¯ã¹ãas
ã䜿ã£ãŠæ»ãå€ã®åãæå®ããããšãã§ããŸãããäžéšã®ããããã£ã«æ³å®å€ã®å€ãå
¥ã£ãŠããããããããå¿
èŠãªããããã£ãç¡ããšãã£ãå Žåã§ããããŒã¿ååŸæã«ãšã©ãŒã¯èµ·ãããŸãããå®éã«ãã®ããããã£ãåç
§ããã¿ã€ãã³ã°ã§åããŠãšã©ãŒãçºçããŸãã
ã»ãšãã©ã®å Žåã¯ã©ããã§ããŒã¿ã®å€ã䜿ãã®ã§ãéçºæã«ãã°ãçºèŠããããšãã§ããŸããããããå®éã«ãã®å€ãåç
§ãããŸã§ã¯ããŒã¿ã®äžåãåå®çŸ©ã®ãã¹çã«æ°ä»ããŸããããã®åé¡ã¯ããŒã¿ãååŸããŠããã«åãæ€èšŒããåé¡ãããã°ãšã©ãŒã«ãããšããæ¹æ³ã§è§£æ±ºã§ããŸãããããå®çŸããããã«å¿
èŠãªã®ãåã¬ãŒã(Type Guard)ã§ãã
â» fetch APIã§ããŒã¿ãååŸããå ŽåãäŸã«æããŸããããå€éšãµãŒãã¹ã®APIãªã©ã¯ã¬ã¹ãã³ã¹ãæ¥ã«å€ããããšã¯ãªãã®ã§ã¬ãã¬ãã«åã¬ãŒãããã¡ãªããã¯ããŸããªããšæã£ãŠããŸããç§ã®å Žåã¯POSTãªã¯ãšã¹ãã«å«ããBodyã®äœææãªã©ãObjectãçŽæ¥æäœããè€éãªåŠçã®åŸã§ããªããŒã·ã§ã³ãè¡ãéã«äœ¿ãã±ãŒã¹ãå€ãã§ãã
以äžã®äŸã§ã¯string
åãnumber
åããå€å®ããŠããŸãã
const foo = "a" as unknown;
if (typeof foo === "string") {
foo.toUpperCase(); // ok: stringåãšããŠæ±ããã
}
if (typeof foo === "number") {
foo * 1; // ok: numberåãšããŠæ±ããã
}
åã¬ãŒãã¯åã¢ãµãŒã·ã§ã³ãšéããå®éã®å€ã®åãå€å®ããŸããã€ãŸãããtypeof value === "string"
ãtrue
ã§ããã°ãvalue
ã¯string
åã§ããããšããããšãä¿èšŒãããŠããŸãã
ãŸãã以äžã®ããã«ããŠåã¬ãŒãé¢æ°(Type Guardé¢æ°)ãå®çŸ©ããããšãã§ããŸãã
// stringåãã©ããããã§ãã¯ããé¢æ°
const isString = (value: unknown): value is string =>ãtypeof value === "string";
if (isString(foo)) {
foo.toUpperCase(); // ok: stringåãšããŠæ±ããã
foo * 1; // error!
}
é¢æ°ã®æ»ãå€ã®åãvalue is Type
ãšããä»»æã®æ¡ä»¶æãreturnããããšã§å®çŸ©ã§ããŸãã
â» äžèšã®isString()ã®äŸã§ãããããã«ãåã¬ãŒãé¢æ°ã䜿ããštypeof value === "string"
ãšæžãã®ã«æ¯ã¹ãŠã¹ãããªæžãããšãã§ããã®ã§äŸ¿å©ã§ãããäžèº«ã®æ¡ä»¶æããæºããã°stringåãšããŠæ±ãããŠããŸããšããç¹ã«ã¯æ³šæãå¿
èŠã§ãã
åã¬ãŒãé¢æ°ãå®çŸ©ããããšã§ãè€æ°ã®ããããã£ããã£ãObjectã®åã¬ãŒããå®çŸã§ããŸãã
type Foo = {
a: string;
b: number;
}
const foo = {
a: "a",
b: 1,
} as unknown;
// å
šãŠã®ããããã£ã"Optional"ãšããå
šãŠã®å€ã«ã€ããŠunknownåãšããåãè¿ã
type WouldBe<T> = { [P in keyof T]?: unknown };
// Objectãã©ãããå€å®ããåã¬ãŒãé¢æ°
const isObject = <T extends Record<string, unknown>>(value: unknown): value is WouldBe<T> =>
typeof value === "object" && value !== null;
// Fooåãã©ãããå€å®ããåã¬ãŒãé¢æ°
const isFoo = (value: unknown): value is Foo =>
isObject<Foo>(value) && typeof value.a === "string" && typeof value.b === "number";
if (isFoo(foo)) {
foo.a.toUpperCase(); // ok
foo.b * 1; // ok
}
äžã®äŸã¯@suinããã®ãã¡ãã®èšäºã§çŽ¹ä»ãããŠããã³ãŒããšã»ãŒåããªã®ã§ã詳ãã解説ã¯çããŸããçŽ æŽãããèšäºãããããšãããããŸãïŒ
https://qiita.com/suin/items/e0f7b7add75092196cd8
åã¬ãŒãé¢æ°ã䜿ãã°ãè€æ°ã®ããããã£ãæã€è€éãªObjectã®åã¬ãŒããå®è£
ããããšãã§ããŸããç§ãã€ãæè¿ãŸã§as
ãå€çšããŠããã®ã§ãããåã¬ãŒãé¢æ°ã®ååšãç¥ã£ãŠããã¯å¿
èŠã«å¿ããŠãã£ããåã¬ãŒããããããã«ãªããŸãããããããObjectã®åã¬ãŒããå®è£
ããäžã§ããã€ãåé¡ãèµ·ããŸããã
isFoo
ã®äŸãèŠãã°ãããéããã³ãŒãã®èŠéããæªãã§ãã Foo
åã®ãªããžã§ã¯ããããããããã£ãå€ããtypeof
æŒç®åãinstanceof
æŒç®åã§ãã§ãã¯ã§ããªãå ("a" | "b"
ãstring[]
ãªã©) ããããšãããã«è€éã«ãªããŸãã
// unionåãå®çŸ©
const Lang = {
ja: "ja",
en: "en",
} as const;
type Lang = typeof Lang[keyof typeof Lang]; // "ja" | "en"
const langList = Object.values(Lang); // ["ja", "en"]
// Poståãå®çŸ©
type Post = {
id: number;
author: string | null;
body: string;
lang: Lang;
isPublic: boolean;
createdAt: Date;
tags?: string[] | null;
};
// Postã®åã¬ãŒãé¢æ°
const isPost = (value: unknown): value is Post =>
isObject<Post>(value) &&
typeof value.id === "number" &&
(typeof value.author === "string" || value.author === null) &&
typeof value.body === "string" &&
langList.includes(value.lang as any) &&
typeof value.isPublic === "boolean" &&
value.createdAt instanceof Date &&
(
value.tags == null ||
(
Array.isArray(value.tags) &&
value.tags.every((item) => typeof item === "string")
)
);
// Poståã®æ¡ä»¶ãæºããããŒã¿
const data = {
id: 1,
author: "taro",
body: "Hello!",
lang: "ja",
isPublic: true,
createdAt: new Date(),
tags: ["tag1", "tag2"],
} as unknown;
if (isPost(data)) {
console.log(data.body.trim()); // ok
}
ä»åã¯åã¬ãŒãé¢æ°ã䜿ããã«å®è£
ããŠã¿ãŸãããã芧ã®éããã®ãŸãŸã§ã¯å¯èªæ§ãäœãããšãŠãèŠã¥ããã§ãã次ã«ãisString()
ã®ãããªç¬èªã®åã¬ãŒãé¢æ°ãçšããŠisPost()
ããªãã¡ã¯ã¿ãªã³ã°ããŠã¿ãŸãã
ãªãã¡ã¯ã¿ãªã³ã°ããã³ãŒãããã¡ãã§ãã
// ãªãã¡ã¯ã¿ãªã³ã°ããisPost()
const isPost = (value: unknown): value is Post =>
isObject<Post>(value) &&
isNumber(value.id) &&
(isString(value.author) || isNull(value.author)) &&
isString(string) &&
isList(value.lang, langList) && // Langåã®åã¬ãŒãé¢æ° ("ja" | "en")
isBoolean(value.isPublic) &&
isDate(value.createdAt) &&
(isNull(value.tags) || isUndefined(value.tags) || isArray(value.tags, isString));
ããã§å¯èªæ§ã®äœãã¯å°ãæ¹åã§ããŸãããisList() ã ã説æããŠãããšã第äžåŒæ°ã§æž¡ããå€ã第äºåŒæ°ã§æž¡ãããé
åã«å«ãŸãããã©ããããã§ãã¯ããåã¬ãŒãé¢æ°ã«ãªã£ãŠããŸãã
åã¬ãŒãé¢æ°ã®å®çŸ©ã®éšåã¯çããŸããããä»åã®ãªãã¡ã¯ã¿ãªã³ã°ã§äœ¿çšããé¢æ°ã ãã§ãå®çŸ©ããã®ãããªã倧å€ã§ããisString()
ã®ãããªããªããã£ãåã®åã¬ãŒãé¢æ°ã¯ç¹ã«ããã§ãããåºæ¬çãªåã¬ãŒãé¢æ°ã¯æ¯ååãå®è£
ã«ãªãã®ã§ãã©ã€ãã©ãªãšããŠãŸãšããŠçšæãããŠãããšäŸ¿å©ã§ãã
é¢æ°ã®å¯èªæ§ã®åé¡ã¯ãªããªããŸããããå®éã«åã¬ãŒãé¢æ°ã䜿ã£ãŠã¿ããšãŸãåé¡ãèµ·ãããŸããã
const post = await fetchPost();
if (!isPost(post)) throw new Error("post is invalid.")
// postã§ãªã«ããã
ããã¯åã®æ€èšŒã«å€±æããéã«ãšã©ãŒãæãããšããåŠçã§ãã
åã¬ãŒãèªäœã¯ãã£ããã§ããŠããã®ã§ããããã®ãšã©ãŒã¡ãã»ãŒãžããã¯ãã©ã®ããããã£ãããããã®ãããããã«å€å¥ããããšãã§ããŸãããpost
ã®äžèº«ãconsole.log
ã§ç¢ºèªãããªã©ãå®éã®å€ãšåã¬ãŒãé¢æ°ã®äžèº«ãæ¯èŒããŠééãæ¢ããããå¿
èŠããããŸããObjectã®ããããã£ã®æ°ãå€ãã»ã©å€§å€ã§ãã
ããã解決ããããã«ã¯åã¬ãŒãé¢æ°ãã©ãããããšã©ãŒçºçæã«"åé¡ã®ããããããã£"ã®æ
å ±ãå«ããšã©ãŒæãåãããé¢æ°ãå®è£
ããå¿
èŠãããããã§ãã
å眮ããé·ããªã£ãŠããŸããŸãããã以äžã®åé¡ã解決ããããã«äœã£ãã©ã€ãã©ãªãtypescannerã§ãã以äžã«ã€ããŠé çªã«è§£èª¬ããŠãããŸãã
â» çŸåšã®ä»æ§ãšã¯ç°ãªãç®æããããŸãã詳现ã¯READMEãã芧ãã ããã
1. isString()ãå«ãåºæ¬çãªåã¬ãŒãé¢æ° + ç¬èªã®åã¬ãŒãé¢æ°
2. Objectã®åã¬ãŒãé¢æ°ãçŽæçã«å®çŸ©ã§ãã scanner() é¢æ°
3. åã¬ãŒããè¡ã£ãäžã§æ€èšŒæžã¿ã®å€ãåãåã scan() é¢æ°
typescannerã§çšæããŠããåã¬ãŒãé¢æ°ã¯ä»¥äžã®ãšããã§ããisArray
以éã®åã¬ãŒãé¢æ°ã¯ç¬¬äžåŒæ°ã«æ€èšŒãããå€ã第äºåŒæ°ä»¥éã«æ€èšŒã«å¿
èŠãªãã®(åã¬ãŒãé¢æ°ãé
åãã³ã³ã¹ãã©ã¯ã¿ãŒãªã©)ãåãåã圢ã«ãªã£ãŠããŸãã
// primitive
isString("a") // true
isNumber(1) // true
isBoolean(true) // true
isUndefined(undefined) // true
isNull(null) // true
isDate(new Data()) // true
isSymbol(Symbol("a")) // true
isBigint(BigInt(1)) // true
// isObject
isObject<T>(value) // <T>(value: unknown) => value is WouldBe<T>
// isArray
isArray(["a", "b"], isString) // string[]
isArray<string | number>(["a", 1], isString, isNumber) // (string | number)[]
isArray(["a", null, undefined], isString, isNull, isUndefined) // (string | null | undefined)[]
// isOptional
isOptional("a", isString) // true
isOptional(undefined, isString) // true
// isList
isList("ja", langList) // true
// isInstanceOf
try {
...
} catch (error) {
if (isInstanceOf(error, Error)) {
error.message // OK
}
}
scanner()
é¢æ°ã¯Objectã®åã¬ãŒãé¢æ°ãçŽæçã«å®çŸ©ã§ããé¢æ°ã§ãã
scanner()
é¢æ°ã䜿ã£ãŠå
çšã®äŸã§ãç»å ŽããPost
åã®åã¬ãŒãé¢æ°ã§ããisPost()
ãæžãæããŠã¿ãŸãã
type Post = {
id: number;
author: string | null;
body: string;
lang: Lang;
isPublic: boolean;
createdAt: Date;
tags?: string[] | null;
};
// scanner()é¢æ°ã§å®çŸ©ããPoståã®åã¬ãŒãé¢æ°
const isPost = scanner<Post>({
id: number,
author: union(string, Null),
body: string,
lang: list(langList),
isPublic: boolean,
createdAt: date,
tags: optional(array(string), Null),
});
scanner()
é¢æ°ã¯ãžã§ããªã¯ã¹ã§æå®ããåã«åãã㊠åããããã£ã«å¯ŸããŠåã¬ãŒãé¢æ°ãèšå®ãã ããšã§Objectã®åã¬ãŒãé¢æ°ãè¿ããŠãããŸããã€ãŸããscanner()
ã®åŒæ°ãšããŠæž¡ããŠããObjectã®äžã§ç»å Žããstring
,number
,list()
,optional()
ãªã©ã¯åã¬ãŒãé¢æ°ïŒãããã¯åã¬ãŒãé¢æ°ãè¿ãé¢æ°ïŒã§ãã
⻠以éstring
ãoptional()
ã®ãããªscanner()é¢æ°ã䜿çšããéã«äœ¿ãåã¬ãŒãé¢æ°ãfieldsãšåŒã³ãŸããstring
ã®ãããªããªããã£ãåã®fieldsã®äžèº«ã¯isString()
ãšå
šãåãã§ãããType Ailiasã§åå®çŸ©ããæèŠã«è¿ã¥ãããã£ãã®ã§ãã®ããã«ããŠããŸã("çŽæçã«å®çŸ©ã§ãã"ã¯ããããããŠããŸã)ã
ãã¡ãã奜ã¿ã§ãªããã°string
ã®ä»£ããã«isString
ãšæžãããšãå¯èœã§ãã
ãŸããç¬èªã®åã¬ãŒãé¢æ°ãçšããŠfieldsãæ¡åŒµããããšãå¯èœã§ãã
type Foo = {
a: string;
b: number;
c: boolean;
d: Date;
e: string[];
f?: string;
g: "a" | "b" | "c";
h: string | null;
i: string | number;
j: number;
};
// ç¬èªã®åã¬ãŒãé¢æ° (field)
const even = (value: unknown): value is number =>
isNumber(value) && value % 2 === 0;
const isFoo = scanner<Foo>({
a: string,
b: number,
c: boolean,
d: date,
e: array(string),
f: optional(string),
g: list(["a", "b", "c"]),
h: union(string, Null),
i: union<string | number>(string, number),
j: even, // Custom field
});
ãããŠåã¬ãŒãé¢æ°ã®åé¡ç¹ã§æãããã©ã®ããããã£ã«åé¡ãããã®ããããã¥ããããšããåé¡ã¯ã"åé¡ã®ããããããã£"ã®æ
å ±ãå«ããšã©ãŒæãåãããããšã§è§£æ±ºããŠããŸãã
å°å³ãªæ©èœã§ã¯ãããŸããããšãŠã䟿å©ã§æ°ã«å
¥ã£ãŠããŸãã
// Error: value.key does not meet the condition.
if (isFoo(data)) {
...
}
scan()
é¢æ°ã¯ç¬¬äžåŒæ°ã«æ€èšŒãããå€ã第äºåŒæ°ä»¥éã«åã¬ãŒãé¢æ°ãæž¡ãããšã§æ€èšŒæžã¿ã®å€ãåãåããŸãã
// success
const data = scan(foo as unknown, isFoo);
data.a // OK
// Error!
const data = scan(bar as unknown, isFoo); // Error: value.key does not meet the condition.
scan()
é¢æ°ã¯Zennã§è©±é¡ã«ãªã£ãŠãã@yuitosatoããã®as-safelyãåèã«ããŸããã
typescannerã®scanner()
é¢æ°ãšäŒŒããããªãã®ãæã€ã©ã€ãã©ãªããããŸããããé¢æ°åãæ°ã«å
¥ããªãã£ãããå
šäœã®èŠéããããŸãè¯ããªãã£ãããã©ã€ãã©ãªåŽã§çšæããŠæ¬²ããã£ãlist()
ãscan()
é¢æ°ã®ãããªæ©èœããªãã£ãããšããããã ïŒããšæãããã®ãèŠã€ããããšãã§ããŸããã§ãããããšã以åããnpmããã±ãŒãžãäœã£ãŠã¿ãããšãé¡æããã£ãã®ã§ä»åã¯èªåã§äœã£ãŠã¿ãŸããã
æ°ã«ãªã£ãæ¹ã¯æ¯éã€ã³ã¹ããŒã«ããŠäœ¿ã£ãŠã¿ãŠãã ããïŒPull Requestçã倧æè¿ã§ãã
typescanner - npm https://www.npmjs.com/package/typescanner
åã¬ãŒã - TypeScript Deep Dive æ¥æ¬èªç https://typescript-jp.gitbook.io/deep-dive/type-system/typeguard