iT邦幫忙

2024 iThome 鐵人賽

DAY 27
0
JavaScript

TypeScript Type Challenges 冒險篇章:30 天闖關之旅,type 簡單了?你確定?系列 第 27

第27關:Permutation!TypeScript 黑板上排列組合:你捨得解開嗎?

  • 分享至 

  • xImage
  •  

第27關:Permutation!TypeScript 黑板上排列組合:你捨得解開嗎?

第27關:Permutation

關卡簡介

Implement permutation type that transforms union types into the array that includes permutations of unions.

實驗一個排列類型 (permutation type),把聯合類型 (union types) 變成一個數組 (array),裡面包含所有可能的排列組合 (permutations)。

任務說明:

type perm = Permutation<'A' | 'B' | 'C'>; 
// ['A', 'B', 'C'] | ['A', 'C', 'B'] | ['B', 'A', 'C'] | ['B', 'C', 'A'] | ['C', 'A'

接下來,你的任務是讓下面的type cases測試通過:

type cases = [
  Expect<Equal<Permutation<'A'>, ['A']>>,
  Expect<Equal<Permutation<'A' | 'B' | 'C'>, ['A', 'B', 'C'] | ['A', 'C', 'B'] | ['B', 'A', 'C'] | ['B', 'C', 'A'] | ['C', 'A', 'B'] | ['C', 'B', 'A']>>,
  Expect<Equal<Permutation<'B' | 'A' | 'C'>, ['A', 'B', 'C'] | ['A', 'C', 'B'] | ['B', 'A', 'C'] | ['B', 'C', 'A'] | ['C', 'A', 'B'] | ['C', 'B', 'A']>>,
  Expect<Equal<Permutation<boolean>, [false, true] | [true, false]>>,
  Expect<Equal<Permutation<never>, []>>,
]

冒險指南:

從以下幾個方向來思考:

  • 聯合類型 (Union Types): 在 TypeScript 中,聯合類型(例如 'A' | 'B' | 'C')表示一個值可以是 'A''B''C' 其中之一。排列的任務就是從聯合類型中提取不同順序的組合。

  • 遞迴解法 (Recursive Approach): 我們需要從聯合類型中每次取出一個元素,然後對剩下的元素進行遞迴排列。

  • 條件型別 (Conditional Types): 條件型別是 TypeScript 用來根據不同條件決定結果類型的功能。在這裡,我們使用它來檢查聯合類型是否已經排列完畢。

  • Exclude 工具類型 (Exclude Utility Type): Exclude<T, U> 是 TypeScript 的內建工具類型,它可以從聯合類型 T 中排除 U 的部分。例如,Exclude<'A' | 'B' | 'C', 'A'> 會返回 'B' | 'C'

通關方式:

解法:


type Permutation<T, K=T> =
    [T] extends [never]
      ? []
      : K extends K
        ? [K, ...Permutation<Exclude<T, K>>]
  : never

細節分析:

  1. 類型定義 (Type Definition)
    type Permutation<T, K=T>:
    我們定義了一個 Permutation 的類型,這個類型有兩個參數:TK
    T 是我們希望生成排列的聯合類型。
    K 參數默認為 T,用來在遞迴過程中代表當前處理的元素。這意味著在初始調用時,K 會與 T 相同,但隨著遞迴的進行,它會逐漸變為 T 中的具體成員。

  2. 基礎情況檢查 (Base Case Check)
    [T] extends [never]:
    這是一個條件型別,用來檢查 T 是否為 never 類型。
    為什麼要這樣寫?: 如果 Tnever,表示已經沒有可供排列的元素了。此時返回一個空數組 [] 作為排列的結束情況,這是遞迴的基礎情況(base case)。這行代碼確保當沒有元素時,排列將停止,並且能正確返回空數組。

  3. 遞迴情況 (Recursive Case)
    K extends K:
    這一行利用 TypeScript 的分配性(distributive)特性。當我們在條件型別中使用 K 時,TypeScript 會自動將 K 的每個成員提取出來進行處理。
    為什麼要這樣寫?: 這使得對於聯合類型的每一個成員,我們都可以進行迭代,並且為每個成員生成其排列。

  4. 主遞迴邏輯 (Main Recursive Logic)
    [K, ...Permutation<Exclude<T, K>>]:
    這一行是遞迴的核心邏輯:

    • K: 這是當前正在處理的元素,將它放在結果的最前面。
    • ...Permutation<Exclude<T, K>>:
      • Exclude<T, K>:這個內建工具類型會從 T 中排除當前的 K。例如,如果 T'A' | 'B' | 'C',且當前 K'A',那麼 Exclude<T, K> 將返回 'B' | 'C'
      • Permutation<Exclude<T, K>>:對剩餘的元素進行遞迴排列。這樣,對於每一個選取的 K,我們都會生成剩下元素的所有排列。
      • ...:展開運算符,將遞迴返回的結果展開並添加到當前 K 前面,形成一個新的元組。
  5. 兌現無法到達的類型 (Fallback Case)
    : never:
    這是條件型別的回退情況,當上述條件不滿足時返回 never。在這個上下文中,這行代碼不會被執行,因為前面的條件已經涵蓋了所有可能的情況。
    為什麼要這樣寫?: 這一行的存在是為了確保類型系統的一致性,並在未滿足條件時給出明確的錯誤。

額外補充:

  • 關於 分配條件類型 (Distributive Conditional Types)

    首先 What is “distributive” ? :

    *An operation that produces the same result when applied to an entire expression as when applied individually to each part and then combining the results.

    (such as multiplication in a(b + c) = ab + ac)

    When conditional types act on a generic type, they become distributive when given a union type. For example, take the following:

    type ToArray<Type> = Type extends any ? Type[] : never;
    

    If we plug a union type into ToArray, then the conditional type will be applied to each member of that union.

    type ToArray<Type> = Type extends any ? Type[] : never;
    
    type StrArrOrNumArr = ToArray<string | number>;
               //^? type StrArrOrNumArr = string[] | number[]
    

    What happens here is that ToArray distributes on:

    string | number;
    

    and maps over each member type of the union, to what is effectively:

    ToArray<string> | ToArray<number>;
    

    which leaves us with:

    string[] | number[];
    

    Typically, distributivity is the desired behavior. To avoid that behavior, you can surround each side of the extends keyword with square brackets.

    type ToArrayNonDist<Type> = [Type] extends [any] ? Type[] : never;
    
    // 'ArrOfStrOrNum' is no longer a union.
    type ArrOfStrOrNum = ToArrayNonDist<string | number>;
             //^? type ArrOfStrOrNum = (string | number)[]
    

這樣,我們就能順利通過測試啦 🎉 😭 🎉

總結:

本次介紹了 Permutation 的實作,下一關會挑戰 Length of String,期待再相見!


上一篇
第26關:Append Argument!TypeScript 變形金剛:參數拼裝車
下一篇
第28關:Length of String!TypeScript 司馬遷,字"子長":字串有多長?
系列文
TypeScript Type Challenges 冒險篇章:30 天闖關之旅,type 簡單了?你確定?30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言