## 11.tupletoObject ```typescript let tuple = ['tesla', 'model 3', 'model X', 'model Y'] as const type TupleToObject<T extends ReadonlyArray<string>> = { [K in T[number]]: K } type result = TupleToObject<typeof tuple> // expected { 'tesla': 'tesla', 'model 3': 'model 3', 'model X': 'model X', 'model Y': 'model Y'} ``` ## as const 首先我們先訂一組 `const route` 記錄所有會使用到的 `const`。 ```typescript const route = { home: "/", admin: "/admin", users: "/users" } ``` 接著我們定義 `goToRoute function` 然後提供 `route params` 去指定 `route` 有哪些 `type` 可以使用,當然你可以單純寫死,`route` 的 `type` ,這會有一個問題,如果 `route` 越多你的 `type` 也要額外加,甚至可能會忘記。 ```typescript const goToRoute = (route: "/" | "/admin" | "/users") => { } ``` 使用上來說也沒問題,可以直接用。 ![截圖 2024-06-12 上午1.21.21](https://hackmd.io/_uploads/ByZvY-IHA.png) 但這時如果我們 `goToRoute` 帶進去的 `params` 他是一個 `reference` 會如何 ?你會看到 `route.admin` 的型別被定義成 `string` 所以導致` type error`,儘管 `route.admin` 的值是正確的。 ![截圖 2024-06-12 上午1.22.59](https://hackmd.io/_uploads/SJdDKbUH0.png) 在沒有用 `as const` 之前 `const object` 對 `ts` 來說對於 `object` 的 `value` 會 `type infer` 成 `Basic Types` 而不是對應的 `value`。 ![截圖 2024-06-12 上午1.24.49](https://hackmd.io/_uploads/rkbtYWUSR.png) 這是因為 `const object` 對 `ts` 來說他是 `mutable` 的所以 `ts` 並無法 `type infer` `object` 中實際的 `value` 是什麼。 ```typescript const route = { home: "/", admin: "/admin", users: "/users" } route.admin = '/another' ``` 那我們要如何讓 `typescript` 知道 `object value` 的 `type` 實際是什麼,這時候 `as const` 就登場了。 ```typescript const route = { home: "/", admin: "/admin", users: "/users" } as const route.admin = '/another' ``` 這時候你會看到 `route.admin` 變成 `readonly` 所以不能再重新賦值給 `route`。 ![截圖 2024-06-12 上午1.35.24](https://hackmd.io/_uploads/SJegiWIBA.png) 然後使用 `reference` 也沒有 `type error`。 ```typescript goToRoute(route.admin) ``` 因為 `route.admin` 已經成功 `type infer` 成 `/admin`。 ![截圖 2024-06-12 上午1.34.19](https://hackmd.io/_uploads/S1OA9b8r0.png) **小結:** `as const` 解決了 * typescript 無法正確 `type infer` 成實際的 `value`。 * 阻止 `const object` `mutable` 特性。 與 `as const` 類似的有 `Object.freeze` 同樣也可以做到 `as const` 的事情,但缺點就是 `Object.freeze` 無法做到 `deep freeze` 的事情而 `as const` 卻可以。 ```typescript const route = Object.freeze({ home: "/", admin: "/admin", users: "/users" }) route.admin = '/another' ``` 接著我們要解決 `route` 的型別定義問題,總不可能要一個一個寫所有的 `const type` 吧XD ```typescript const goToRoute = (route: "/" | "/admin" | "/users") => { } ``` 我們的目標很明確希望拿到 `route` 中所有的 `value type` ,所以首先我們需要定義 `route object type` 是什麼。 在 `ts` 中我們可以使用 `typeof` 去 `infer` 對應變數的型別是什麼,因為我們的 `route` 有使用 `as const` 所以 `ObjectType` 會自動 `infer` 成 `readOnly` 的 `type`。 ![截圖 2024-06-12 上午1.48.51](https://hackmd.io/_uploads/rydf0bLBR.png) 如果加上 `keyof` 就可以 `infer` 對應的 `object key type` 是什麼了。 ![截圖 2024-06-12 上午1.50.45](https://hackmd.io/_uploads/ByLFA-LrR.png) 但我們的需求是 `infer` 出 `route` 的 `value type` ,這邊我們使用 `ts` 的 `mapping type` 功能,結合上述範例在 `Objecttype` 中找到 `infer readonly object type` 再搭配 `ObjectKey` ,這時 `ts` 就會很巧妙的 `infer` 出 `Route` 的所有 `type` 了。 ![截圖 2024-06-12 上午1.53.56](https://hackmd.io/_uploads/HydrkfLHC.png) 這時如果新增新的 `value` ```typescript const route = { home: "/", admin: "/admin", users: "/users", newUser:'/newUser' } as const ``` `type Route` 就自動加上去了~ ![截圖 2024-06-12 上午1.57.21](https://hackmd.io/_uploads/SylzlMISA.png) 最後的 `demo` 如下 ```typescript const route = { home: "/", admin: "/admin", users: "/users", newUser: '/newUser' } as const type Route = (typeof route)[keyof typeof route] // dynamic type for route value const goToRoute = (route: Route) => { } // auth type suggestion goToRoute('/') // 可以 reference goToRoute(route.admin) ``` ## mapping ```typescript import { type ValueOf } from 'type-fest'; import { z } from 'zod'; export const BONUS_CRITERIA_FIELD_TYPE = { SELECT: 0, MULTI_SELECT: 1, RANGE_PICKER: 2, INPUT_NUMBER: 3, AFFILIATE_LIST: 4, PRODUCT_VENDORS: 5, SLIDER: 6, } as const; export type BonusCriteriaFieldType = ValueOf<typeof BONUS_CRITERIA_FIELD_TYPE>; type CriteriaFieldReturnTypeMap = { [BONUS_CRITERIA_FIELD_TYPE.SELECT]: { return: number[] | undefined; value: TargetBonusCriteriaOptions['value']; }; [BONUS_CRITERIA_FIELD_TYPE.MULTI_SELECT]: { return: number[] | undefined; value: TargetBonusCriteriaOptions['value']; }; [BONUS_CRITERIA_FIELD_TYPE.RANGE_PICKER]: { return: { from: number; to: number } | undefined; value: TargetBonusCriteriaDateRange['value']; }; [BONUS_CRITERIA_FIELD_TYPE.AFFILIATE_LIST]: { return: string[] | undefined; value: TargetBonusCriteriaAffiliateCode['value']; }; [BONUS_CRITERIA_FIELD_TYPE.PRODUCT_VENDORS]: { return: string[] | undefined; value: TargetBonusCriteriaProductVendors['value']; }; [BONUS_CRITERIA_FIELD_TYPE.INPUT_NUMBER]: { return: number | undefined; value: TargetBonusCriteriaInput['value']; }; [BONUS_CRITERIA_FIELD_TYPE.SLIDER]: { return: number[] | undefined; value: TargetBonusCriteriaOptions['value']; }; }; type CriteriaFieldInitialValueMapType = { [K in BonusCriteriaFieldType]: (value?: CriteriaFieldReturnTypeMap[K]['value']) => CriteriaFieldReturnTypeMap[K]['return']; }; export const CRITERIA_FIELD_INITIAL_VALUE_MAP: CriteriaFieldInitialValueMapType = { [BONUS_CRITERIA_FIELD_TYPE.SELECT]: (value) => toArray(value?.map(({ id }) => id)), [BONUS_CRITERIA_FIELD_TYPE.MULTI_SELECT]: (value) => value?.map(({ id }) => id), [BONUS_CRITERIA_FIELD_TYPE.RANGE_PICKER]: (value) => value && isNotNil(value.startAt) && isNotNil(value.endAt) ? { from: value.startAt, to: value.endAt, } : undefined, [BONUS_CRITERIA_FIELD_TYPE.AFFILIATE_LIST]: (value) => value?.map(({ affiliateCode }) => affiliateCode), [BONUS_CRITERIA_FIELD_TYPE.PRODUCT_VENDORS]: (value) => value?.flatMap(({ id, vendors }) => vendors.map(vendor => `${id}-${vendor.id}`)), [BONUS_CRITERIA_FIELD_TYPE.INPUT_NUMBER]: (value) => value, [BONUS_CRITERIA_FIELD_TYPE.SLIDER]: (value) => value?.map(({ id }) => id), }; ```