
ã¯ããã«
DIGGLEã¨ã³ã¸ãã¢ã®itoã§ãã
æè¿ã¯å°ãã¥ã¤æãããªã£ã¦ããããã«æãã¦ãã¾ãããææå¯ãæ¥ãæ¥ããã³ã«ä¸å¯å温ã¨ããè¨èãæãè¿ãã¦ãã¾ãã
DIGGLEã§ã¯2023å¹´1æããTypeScriptãå°å ¥ãã¦ããã³ãã¨ã³ãã®éçºããã¦ãã¾ãã
TypeScriptã®åã«ãã£ã¦éçºã«å¤ãã®å®å¿æãããããã¦ããã¾ãããåãé©ç¨ããã°çµããã¨ãã話ã§ã¯ãªãåã®å¹æãé«ããããã®Tipsãå¤ãåå¨ãã¦ãããããããéçºç°å¢ãæ§ç¯ããããã«æ¥ã æ¹åããã¦ããå¿ è¦ãããã¾ãã
ä»åã¯ãã®ãããªTipsã®ä¸ã¤ã§ããBranded Typeãå°å ¥ããæ°å¤åã®ç¨éã«å¿ããã¿ã°ä»ãã«ãã£ã¦æå³ããªãæ¼ç®ãé²ãæ¹æ³ã«ã¤ãã¦ç´¹ä»ãã¾ãã
Branded Typeã¨ã¯
Branded Type*1ã¯ç¬èªã§å®ç¾©ããåãããå¼·åºã«ã§ããTipsã§ãã
åã¨ã¤ãªã¢ã¹
Branded Typeã¯åã¨ã¤ãªã¢ã¹ã¨ã®ç¸æ§ãããTipsã«ãªã£ã¦ãããããåã¨ã¤ãªã¢ã¹ã軸ã«èª¬æãããã¦ããã ãã¾ãã
åã¨ã¤ãªã¢ã¹ã¨ã¯ãåã«å¥åãã¤ãããã¨ãã§ããæ©è½ã§ãã
TypeScriptã®åã¯ããªããã£ãåã¨ãªãã¸ã§ã¯ãã«åãããã¨ãã§ãããããã®ååä½ã®å¥åãçµã¿åãããåã«ååãã¤ãããã¨ãã§ãã¾ãã
type Positive = number // ããªããã£ãåã®åã¨ã¤ãªã¢ã¹ type Cat = { name: string; species: string; } // ãªãã¸ã§ã¯ãã®åã¨ã¤ãªã¢ã¹
åã¨ã¤ãªã¢ã¹ã¯é常ã«ä¾¿å©ãªã®ã§ãããçµå±ã¯åãåã§ããããã«å¥ã®åã¨ãã¦æ±ãããå ´é¢ã§ã¯ä½¿ããªãã£ãããã¾ãã
ä¾ãã°ä¸è¨ã®ãããªç¶æ³ã§ãã
type Positive = number // æ£ã®æ° let positiveNum: Positive = 1 positiveNum = -1 // è² ã®æ°ãä»£å ¥ãã¦ãã¨ã©ã¼ã«ãªããªã
type Unit = { id: string, name: string } type Budget = { id: string, name: string, unitId: number } type Row = { id: string, summary: string, budgetId: number } async function getRows(budgetId: string) { const response = await api.get(`/budget/${budgetId}/rows`) return response.data } const rows = await getRows(unit.id) // ç°ãªããã¼ã¿æ§é ã®IDãæ¸¡ãã¦ãã¨ã©ã¼ã«ãªããªã
ã©ã¡ãã®å ´åãåã¨ãã¦ã¯numberãstringãå®ãããã¦ããã ãã®ãããåæ§ã®åãæã£ã¦ãããã®ã§ããã°ã©ã®ãããªãã¼ã¿ã§ãã£ã¦ãå ¥ã£ã¦ãã¾ãã¾ãã
ãªãã¸ã§ã¯ãã«ããã¨keyãç°ãªãçµã¿åããã«ãããã¨ã§ããç¨åº¦ç·©åãããã¨ã¯ã§ãããã®ã®ãåãkeyã®çµã¿åãããæã£ã¦ãããªãã¸ã§ã¯ãå士ã¯TypeScriptã®æ§é çåä»ãã®ä»çµã¿ã«ãã£ã¦åä¸ã¨ã¿ãªããã¦ãã¾ããã常ã«éç¨ããããã§ã¯ããã¾ããã
ã¯ã©ã¹ã«ãããã¨ã§è§£æ±ºãããã¨ã§ã¯ããã¾ãããã¯ã©ã¹ããå¤ãä¸ã¤åãåºãå ´åãªã©ã«åãåé¡ã«ç´é¢ãããã¨ã«ãªãã¾ãã
ãã®ãããªå ´åã§ãå使¤è¨¼ãããåã®åãã§ãã¯ã®ã¿ã¤ãã³ã°ã§ã¨ã©ã¼ãåºãã¦ãããã¨éçºã®å¹çãä¸ããã¾ãã
Branded Type
Branded Typeãæ´»ç¨ãããã¨ã«ãã£ã¦ãåè¿°ã®åã¨ã¤ãªã¢ã¹ã®ä¾ãããå¼·åºãªåã«ãããã¨ãã§ãã¾ããä¾ãã°ãæ°å¤åã«ãæ£ã®æ°ããããã¤ã³ã座æ¨ããªã©ã®ã¿ã°ãä»ãããã¨ã§ã誤ã£ãä»£å ¥ãæ¼ç®ãé²ããã¨ãã§ãã¾ãã
ããæ¹ã¯è²ã
æ¹æ³ãããã®ã§ãããä¸è¨ã®ãµã¤ãã§ç´¹ä»ããã¦ããæ¹æ³ã¯ã·ã³ãã«ã§ããã¼ã®ã·ã³ãã«(__brand)ãç¨æããå¾ã
åã«ãã®ããã¼ã·ã³ãã«ã使ã£ã& { [__brand]: T }ãå ããã ãã§ã*2ã
declare const __brand: unique symbol type Brand<B> = { [__brand]: B } export type Branded<T, B> = T & Brand<B>
Branded Typeãè¸ã¾ãã¦åè¿°ã®ä¾ã§ç´¹ä»ããã³ã¼ããæ¸ãç´ãã¨ä¸è¨ã®ããã«ãªãã¾ãã
type Positive = Branded<number, 'Positive'> // æ£ã®æ° let positiveNum: Positive = 1 as Positive positiveNum = -1 // å 'number' ãå 'Brand<"Positive">' ã«å²ãå½ã¦ããã¨ã¯ã§ãã¾ãããts(2322)
type Unit = { id: Branded<number, 'UnitId'>; name: string } type Budget = { id: Branded<number, 'BudgetId'>; name: string; unitId: number } type Row = { id: Branded<number, 'RowId'>; summary: string; budgetId: number } async function getRows(budgetId: Branded<number, 'BudgetId'>) { const response = await api.get(`/budget/${budgetId}/rows`) return response.data } const rows = await getRows(unit.id) // å 'Branded<number, "UnitId">' ãå 'Brand<"BudgetId">' ã«å²ãå½ã¦ããã¨ã¯ã§ãã¾ããã // ãããã㣠'[__brand]' ã®åã«äºææ§ãããã¾ããã // å '"UnitId"' ãå '"BudgetId"' ã«å²ãå½ã¦ããã¨ã¯ã§ãã¾ãããts(2345)
ç¡äºã«ã¨ã©ã¼ãåºã¦èª¤ã£ãä»£å ¥ãäºåã«æ¤ç¥ã§ããããã«ãªãã¾ããã
positiveNum = -1ã®ä¾ãpositiveNum = -1 as Positiveã¨ãã¦ãã¾ãã°ä»£å
¥èªä½ã¯ã§ãã¦ãã¾ãã¾ããã-1 as Positiveã¨è¨è¿°ããæç¹ã§éåæãè¦ãããã¨ãã§ããã¨æãã¾ãã
DIGGLEã§ã®Branded Typeã®æ´»ç¨æ¹æ³
DIGGLEã§ã¯æçè¨ç®æ¸(PL)ã表ç¾ããã«ããã£ã¦Canvasã¿ã°ã«ããæç»ãæ´»ç¨ãã¦ãã¾ãã
Branded Typeã¯ãCanvasã¿ã°ã«ãããã¼ãã«ãæ§ç¯ããã¨åºå¥ããããªãä¸è¨ã®äºã¤ã®æ å ±ãé©åã«åºå¥ãããã¨ã«å½¹ç«ã¡ã¾ããã
- Canvasã¿ã°å ã«æç»ããéã®åº§æ¨(Point)
- ãã¼ãã«å ã®ã»ã«ã®ä½ç½®(Index)

Pointãè¨å®ããé¨åã«Indexãæå®ãã¦ãã¾ã£ããããã®éãèµ·ãã£ã¦ãã¾ãã¨Canvasã®æç»ãå´©å£ãã¦ãã¾ãã¾ãã
éçºãã¦ããéã«ãã¼ã«ã«ç°å¢ã§çºçãããã°ã«ã¯ä¸è¨ã®ãããªãã®ããããããããã®åãééããåã¨ã©ã¼ã§æ¤ç¥ã§ããªããã°æ¹åããªãã¡ã¯ã¿ãªã³ã°ã§åçºãããªã¹ã¯ãããã¾ãã
- ãã¼ãã«ã®åºå®è¡ãåãæå®ããã»ã«ã®ä½ç½®(Index)ã«ã誤ã£ã¦Pointã®å¤ãçªã£è¾¼ãã§ãã¾ã
- åºå®è¡ãåã«å¤§ããªå¤ãè¨å®ãããCanvasã¿ã°ä¸é¢ã«åºå®è¡ãåã表示ããã¦ãã¾ã
- 罫ç·ã®æç»ã§ã»ã«ã®ä½ç½®(Index)ãç½«ç·æç»éå§ä½ç½®ã®Pointã«å¤æããå¿
è¦ãããã®ã«Indexã®ã¾ã¾çªã£è¾¼ãã§ãã¾ã
- 罫ç·ãæ£ããæç»ããããã¼ãã«ã®æç»ãå´©ãã
ãã°ã¬ãçºçããªãããã«ã¹ãããã·ã§ãããã¹ããä»è¾¼ããªã©ãã¦ãã¾ããããããã§ãããããããã¨Canvasãã¼ãã«ãæ¹åãããã¨ãããã³ã«æ°ãå¼µããªããã°ããã¾ããã§ããã
Branded Typeåã«ãã£ã¦åãééããäºåã«ã¨ã©ã¼ã§æ¤ç¥ã§ããããã«ãªããæ°ãå¼µããã«æãåã£ãæ¹åãããããç°å¢ãã§ãã¾ããã
Canvasã¿ã°ã§ã®å®éã®æ´»ç¨æ¹æ³
DIGGLEã§ã¯ä¸è¨ã®è¨äºãåèã«ããã¦ããã ããBranded Typeãç¨æãã¦ãã¾ãã
declare const __brand: unique symbol type Brand<T, TrueKeys extends string, FalseKeys extends string = never> = T & { [__brand]: { readonly [key in FalseKeys | TrueKeys]: key extends TrueKeys ? true : false } }
ããã®ã¾ã¾ã§ãããªãæç¨ãªã®ã§ãããPoint/Indexã§æ´»ç¨ãããã¨ããã¨ååæ¼ç®ã§åã®æ å ±ãå¹ãé£ã¶åé¡ãçºçãã¾ãã
type Point = Brand<number, 'Point'> const pointA = 100 as Point const pointB = 200 as Point const pointC: Point = pointA + pointB // Pointåã§ã¯ãªãnumberåã«ãªãããã¨ã©ã¼ãçºçãã
asã§ååæ¼ç®ã®ãã³ã«ç¡çãããã£ã¹ããããã¨ã¯ã§ããã®ã§ãããååæ¼ç®ã®ä¸ã§åãæ··å¨ãããã¨ãã§ãã¦ãã¾ãã¾ãã
type Point = Brand<number, 'Point'> type Index = Brand<number, 'Index'> const pointA = 100 as Point const pointB = 200 as Point const pointC: Point = (pointA + pointB) as Point // Pointåã«ãªã const indexA = 1 as Index const num: Point = (pointA + indexA) as Point // ç¹ã«ã¨ã©ã¼ã«ãªããªã
Canvasã§ã®ãã¼ãã«æç»ã§ã¯ç ©éãªååæ¼ç®ãè¡ããããããåã®åãééãã§ã¨ã©ã¼ãèµ·ããå±éºæ§ãé«ããã§ããã°å®å ¨ã«å¯¾å¿ãããé¨åã§ããã
ããã§numberåã«ã ãBrandedNumberã¨ããå/å¤ãç¨æãã¦ãå¤ã®ã»ãã«åã®åé¡ãè§£æ¶ããååæ¼ç®ãä»è¾¼ããã¨ã«ãã¾ããã
export type BrandedNumber< TrueKeys extends string, FalseKeys extends string = never, > = Brand<number, TrueKeys, FalseKeys> export const BrandedNumber = <Z extends z.Schema>(schema: Z) => <T extends number>() => { const brandedNumber = (num: number): T => { const result = schema.safeParse(num) if (result.success) return result.data as T console.error(result.error) throw new Error(`Validation error, ${schema.description}`, result.error) } brandedNumber.add = (...args: T[]): T => args.reduce((acc: number, cur: number) => acc + cur, 0) as T brandedNumber.subtract = (first: T, ...rest: T[]): T => { if (first === undefined) throw new Error('First argument is required') return rest.reduce((acc: number, cur: number) => acc - cur, first) as T } brandedNumber.multiply = (a: T, b: number): T => (a * b) as T brandedNumber.divide = (a: T, b: number): T => { if (b === 0) throw new Error('Division by zero') return (a / b) as T } brandedNumber.z = schema as Z return brandedNumber }
BrandedNumberãå©ç¨ããã¨ä¸è¨ã®ããã«å®å ¨ã«ååæ¼ç®ãã§ããããã«ãªãã¾ãã
export type Point = BrandedNumber<'Point', 'Index' | BrandedNumberType.NaN> const schema = z.coerce.number().default(-1).describe('Point') export const Point = BrandedNumber(schema)<Point>() const add = Point.add(Point(1), Point(2)) const sub = Point.subtract(Point(1), Point(2)) const mul = Point.multiply(Point(1), 5) const div = Point.divide(Point(1), 5)
ã¤ãã§ã«ãªãã¹ãasã使ããªãã¦æ¸ãããã«Point(1)ã®å½¢ã§å¯¾è±¡ã®åã®å¤ãä½ãããããªé¢æ°ãä»è¾¼ã¿ã¾ããã
ä»è¾¼ãã 颿°ã§ã¯zodã®parseãä»è¾¼ããããã«ãã¦ãã¾ã(zodãä»è¾¼ããããã«ãããã¨ã§Position(-1)ã¨å¤ã使ããéã«ã¨ã©ã¼ãåºããããã«ãªãã¾ãã)ã
export type BrandedNumber< TrueKeys extends string, FalseKeys extends string = never, > = Brand<number, TrueKeys, FalseKeys> /// NOTE: é¨åç忍è«ãå¹ããªããããã«ãªã¼åãããã¨ã«ãã£ã¦schemaã®ã»ãã«åæ¨è«ãå¹ãããã«ãã¦ãã /// ãã®ãããBrandedNumber(schema)<T>()ã®ããã«å©ç¨ãã export const BrandedNumber = <Z extends z.Schema>(schema: Z) => <T extends number>() => { const brandedNumber = (num: number): T => { const result = schema.safeParse(num) if (result.success) return result.data as T console.error(result.error) throw new Error(`Validation error, ${schema.description}`, result.error) } ... return brandedNumber }
ããããã«ãªã¼åããã¦ããconst brandedNumber = (num: number): Tã®é¨åã¯æ¬å½ã¯ä¸è¨ã®ããã«ãããã£ãã§ãã
export type BrandedNumber< TrueKeys extends string, FalseKeys extends string = never, > = Brand<number, TrueKeys, FalseKeys> export const BrandedNumber = <T extends number, Z extends z.Schema>(schema: Z) => { const brandedNumber = (num: number): T => { const result = schema.safeParse(num) if (result.success) return result.data as T console.error(result.error) throw new Error(`Validation error, ${schema.description}`, result.error) } ... return brandedNumber }
ã§ãããã³ã¡ã³ãé¨åã«è¨è¼ãã¦ããéãTãæå®ãã¦ãã¾ãã¨å¼æ°ã«zodã®schemaãæ¸¡ãã¦ãZã®åæ¨è«ããããªããªã£ã¦ãã¾ã£ããããã«ãªã¼åãããã¨ã§æ¨è«ãå¹ããã¦å©ç¨ã§ããããã«ãã¦ãã¾ãã
交差åã®æ±ãã«ã¤ãã¦
åèã«ããã¦ããã ããä¸è¨ã®è¨äºã§ã¯ã
Branded Type ã®ãªãã¸ã§ã¯ãåã®valueã«unknownã§ã¯ãªãtrue/falseãæããã
ã¨ãã工夫ã§åãkeyã§ããããtrue/falseãæå®ããã¨å
¨ä½ãneveråã«ãªãããã«ãã¦ãã¾ããã
type PositiveNumber = number & { Positive: true };
type NegativeNumber = number & { Positive: false };
type PositiveNegativeNumber = PositiveNumber & NegativeNumber; // never
true/falseã§åãããã夿°ã®è¦ç´ ã§æ§æããã°Positive/Negativeã¨ãã£ã2å¤ã®é¢ä¿ã®ãã®ä»¥å¤ã§ã表ç¾ãããã¨ãã§ããããã¨ã¦ã便å©ãªããæ¹ãªã®ã§ããã
ä»åã®å¯¾å¿ã§ã¯ããã¼ã·ã³ãã«ã§ãã__brandããã¾ãã¦ãã¾ã£ã¦ãããããneverã«ãªã£ã¦ããkeyãneverã«ãªãã ãã§åããããªæ¯ãèãã«ãªãã¾ããã
type PositiveNumber = number & { [__brand]: { Positive: true } } type NegativeNumber = number & { [__brand]: { Positive: false } } type PositiveNegativeNumber = PositiveNumber & NegativeNumber // number & { [__brand]: never }
ãã®å ´åã®PositiveNegativeNumberã__brandãã¼ã«neveråãå
¥ã£ã¦ã¯ããã®ã§ããããã¼ã¹ã«ãã¦ããåãåãã§ããã°åä¸ãã¼ã§true/falseãæå®ãã¦ããåã®äº¤å·®åãã¨ã£ããã®å士ã®ä»£å
¥ã¯ã§ãã¦ãã¾ãã¨ããä¸éå端ãªç¶æ
ã«ãªã£ã¦ãã¾ãã¾ãã
åãè¦ãéã«neveråã§ããã°ããã«ééãã«æ°ãã¤ãã¾ãããnumber & { [__brand]: never }ã¨ããåã«ãªã£ã¦ããå ´åã¯æå³ãã¦ãããã®ã誤ããªã®ãåããã¥ããç¶æ³ã«ãªã£ã¦ãã¾ãã¾ãã
type TrueType = number & { [__brand]: { HogeHoge: true } } type FalseType = number & { [__brand]: { HogeHoge: false } } type HogeHogeNumber = TrueType & FalseType const testNum: HogeHogeNumber = 1 as PositiveNegativeNumber // HogeHogeNumberãnumber & { [__brand]: never }ã«ãªããããPositiveNegativeNumberãå ¥ãã¦ãã¨ã©ã¼ã«ãªããªã
ããã§äº¤å·®åãä½ãAndBrandåãç¨æãã¾ããã
export type AndBrand<T, U> = T extends { [__brand]: infer TO } ? U extends { [__brand]: infer UO } ? TO & UO extends never ? never : T & U : never : never
ãã¡ãã使ãã¨ä¸è¨ã®ããã«ãªãåé¡ãè§£æ¶ããã¾ãã
type TrueType = number & { [__brand]: { HogeHoge: true } } type FalseType = number & { [__brand]: { HogeHoge: false } } type HogeHogeNumber = AndBrand<TrueType, FalseType> // neveråã«ãªããã¨ã§ééãã«æ°ãä»ãããããªã
ã¾ã¨ã
Branded Typeåã«ãã£ã¦åãå¼·åºã«ãã¦ä»å¾ã®ã¬ãã¼ãæ¹åã«ããã¦èªä¿¡ãæã£ã¦æãå ãã¦ããããããªä½å¶ãæ´ãã¾ããã
ä»åã®å¯¾å¿ä¸ã«ããåã誤ã£ã¦æå®ãã¦ããé¨åãè¦ã¤ãããªã©æ©é广ãçºæ®ãã¦ããã¾ããã
ä»å¾ããã®Branded Typeåãåºãé²ãã¦ãããã¨ã«ãã£ã¦ããããããã³ãã¨ã³ãç°å¢ãæ´ãã¦ãããã¨æã£ã¦ãã¾ãã
çµããã«
DIGGLEã§ã¯ã¨ãã«ãããã¯ããéçºãã¦ãããã¨ã³ã¸ãã¢ã大åéä¸ã§ãã
å°ãã§ãèå³ãããã°ããã²ä¸è¨æ¡ç¨ãµã¤ãããã¨ã³ããªã¼ãã ããã
ã«ã¸ã¥ã¢ã«é¢è«ã宿½ãã¦ããã®ã§ãæ°è»½ãªãæ°æã¡ã§ãå¿åããã ããã°ã¨æãã¾ãï¼
*1:ä»ã®å¼ã³æ¹ã¨ãã¦å¹½éå(Phantom Type)ãBranded Primitiveã¨ããå¼ã³æ¹ãããã¦ããããã§ã
*2:ããã¼ã·ã³ãã«ãå«ãã ãªãã¸ã§ã¯ãã交差åã§ä»ä¸ãããã¨ã§ããªãã¸ã§ã¯ãã®ãã¼ã®åãã§ãã¯ã§å¼ã£ãããããã«ãããã®ã§ã ãã®ãããããã¼ã·ã³ãã«ãå«ãã ãªãã¸ã§ã¯ããåãæ§é ã«ãªã£ã¦ããå ´åã¯åãBranded Typeã¨ãã¦èªèããã¾ã https://michalzalecki.com/nominal-typing-in-typescript/#approach-4-intersection-types-and-brands