iT邦幫忙

2024 iThome 鐵人賽

DAY 25
1
JavaScript

我推的TypeScript 操作大全系列 第 25

我推Day25 - 讓 TypeScript 炸裂你的腦袋!用映射型別打造超型別安全的解析器技巧

  • 分享至 

  • xImage
  •  

https://ithelp.ithome.com.tw/upload/images/20241009/20124462IgP1yGo2Xn.jpg


Functor 和映射型別,優雅地解析複雜數據,讓你的程式碼精美得像藝術品!🎨💥

深入探索 TypeScript 中的型別安全解析與映射型別

🌟 你是否曾經因為解析字串資料而感到頭痛?
總覺得解析器的邏輯複雜得像一團毛線球?

別擔心!今天,我們要一起來拆解這個毛線球,把它變成一個可愛的小貓玩具 🧶😸。
透過 TypeScript 的型別安全解析器,不僅能讓你更自信地處理資料,還能讓程式碼更有彈性!

我們的目標很簡單:設計出一個既型別安全又易於使用的解析器,同時保持 API 的簡潔性,讓每次開發都變得更有趣、更順手!

這次我要分享一個既型別安全又易於使用的解析器設計方法。目標是保持解析器的可組合性,同時維持簡單易懂的 API。

什麼是解析器(Parser)?

解析器的核心概念並不複雜。我們可以將它視為一個物件,這個物件包含一個 run 函數,用來接收一個字串並回傳解析結果(ParserResult)。這個解析結果包括兩部分:

  1. 解析後的資料:從字串中提取出來的有用信息。
  2. 剩餘的字串:還未被解析的部分。

例如,假設我們有一段字串 123abc,我們希望提取前面的數字部分,解析器就可以把 123 當作解析後的資料,而把 abc 當作剩餘的字串。

這樣的處理方式很像閱讀一本書:每次讀取一段內容,然後留下未讀部分供後續使用。這種逐步處理的特性讓我們可以鏈式操作解析器,也就是所謂的 Monad 特性。

建立我們的第一個解析器

讓我們從一個簡單的例子開始:假設我們需要解析固定長度的字串。在某些情境下,例如處理固定寬度的文件(每一欄位的長度固定),這個解析器特別實用。

範例程式碼如下:

class FixedWidthParser {
  constructor(private length: number) {}
  
  run(input: string): ParserResult<string> {
    const parsed = input.slice(0, this.length);
    const remaining = input.slice(this.length);
    return { parsed, remaining };
  }
}

// 使用例子
const parser = new FixedWidthParser(3);
const result = parser.run('123abc');
console.log(result); 
// { parsed: '123', remaining: 'abc' }

這段程式碼的解析器會截取字串的前三個字符作為結果,而剩餘的部分則被保留下來。

型別安全的解析

當我們開始解析資料時,不僅希望解析器能夠提取內容,還希望它能精確地處理特定的型別。例如,我們可能需要解析數字、日期或布林值等不同類型的資料。

這時,我們可以利用 Functor 的概念來實現這種功能。Functor 可以讓我們定義 map 函數,將解析器的結果從一種類型轉換到另一種類型。舉個例子:

class Parser<A> {
  constructor(private runParser: (input: string) => ParserResult<A>) {}

  map<B>(fn: (a: A) => B): Parser<B> {
    return new Parser((input) => {
      const { parsed, remaining } = this.runParser(input);
      return { parsed: fn(parsed), remaining };
    });
  }
}

// 使用 map 將字串轉換為數字
const numberParser = new Parser<string>((input) => ({ parsed: input.slice(0, 2), remaining: input.slice(2) }))
  .map((value) => parseInt(value, 10));

const result = numberParser.run('42abc');
console.log(result); 
// { parsed: 42, remaining: 'abc' }

這裡,我們利用 map 函數將原本的字串結果轉換為數字,這樣就可以用同樣的解析器來處理不同類型的資料。map 函數讓我們能在不更改原解析器邏輯的情況下,靈活地變換輸出型別。

解析複合物件

解析單一的基本資料型別後,我們可能會遇到更複雜的情境,比如解析 JSON 物件或組合多個欄位。這時,我們需要將多個解析器組合在一起,解析出完整的物件。

這裡我們可以利用 TypeScript 的映射型別(Mapped Types),它能夠將一種類型映射到另一種類型,並確保每個屬性都有對應的解析器。這樣,我們能把每個解析邏輯模組化,然後組合成一個完整的解析流程。

使用映射型別實現更簡潔的解析邏輯

在 TypeScript 中,映射型別是一種強大的工具,它能讓我們更輕鬆地定義選擇性屬性(Partial)或唯讀屬性(Readonly)。我們來看看如何使用映射型別減少重複程式碼:

type MyPartial<T> = {
  [P in keyof T]?: T[P];
};

// 使用 MyPartial 產生部分選擇性的 User 類型
type User = {
  name: string;
  password: string;
  address: string;
  phone: string;
};

type UserPartial = MyPartial<User>;
// 等同於
// {
//   name?: string;
//   password?: string;
//   address?: string;
//   phone?: string;
// }

這段程式碼展示了如何利用映射型別來創建選擇性屬性,使我們能夠靈活地控制每個屬性的選擇性。

進階映射型別:重新命名屬性

TypeScript 4.1 引入了 as 語法,讓我們可以重新命名屬性。例如,我們希望自動生成一個物件的 getter 方法,就可以這樣做:

type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};

interface Person {
  name: string;
  age: number;
  location: string;
}

type LazyPerson = Getters<Person>;
// 結果:
// {
//   getName: () => string;
//   getAge: () => number;
//   getLocation: () => string;
// }

這個範例展示了如何透過映射型別生成新的方法,這樣我們就能自動為物件中的每個屬性創建對應的 getter 方法,節省重複的程式碼。

實用場景:解析用戶資料

假設我們有一個情境,需要從 API 返回的原始字串中解析出使用者資料,並確保每個屬性都符合預期的類型。我們可以定義不同的解析器來處理每個欄位,並將它們組合起來:

interface User {
  id: number;
  name: string;
  email: string;
}

const idParser = new Parser<string>((input) => ({ parsed: input.slice(0, 2), remaining: input.slice(2) }))
  .map((value) => parseInt(value, 10));

const nameParser = new Parser<string>((input) => ({ parsed: input.slice(0, 5), remaining: input.slice(5) }));
const emailParser = new Parser<string>((input) => ({ parsed: input.slice(0, 10), remaining: input.slice(10) }));

// 定義一個映射型別來匹配 User 的屬性到相對應的解析器
type ParserDef<T> = { [P in keyof T]: Parser<T[P]> };

const userParsers: ParserDef<User> = {
  id: idParser,
  name: nameParser,
  email: emailParser
};

// 最後我們可以利用這些解析器來解析整個 User 物件
function parseUser(input: string): User {
  const { parsed: id, remaining: rest1 } = userParsers.id.run(input);
  const { parsed: name, remaining: rest2 } = userParsers.name.run(rest1);
  const { parsed: email } = userParsers.email.run(rest2);
  return { id, name, email };
}

const result = parseUser('42JohnSmithjohn@example.com');
console.log(result); 
// { id: 42, name: 'JohnS', email: 'john@example' }

在這個範例中,我們利用 ParserDef 確保每個屬性都有對應的解析器,然後逐步解析輸入字串,最終返回完整的使用者物件。
這樣的設計既型別安全,又讓解析邏輯變得模組化。


當然可以,這裡是重點小結語和勵志的結尾語:


重點小結語

📌 什麼是解析器?
解析器是一個可以讀取字串並回傳解析結果的物件,能夠逐步處理字串資料,實現型別安全。

📌 Functor 與型別安全解析
透過 Functor,我們可以使用 map 轉換解析結果,讓解析器輕鬆處理不同的資料型別(例如字串轉數字)。

📌 映射型別的威力
TypeScript 的映射型別讓我們能定義選擇性(Partial)或唯讀(Readonly)屬性,減少重複程式碼,提升開發效率。

📌 進階技巧:重新命名屬性
利用 TypeScript 4.1 的 as 語法,可以自動生成 getter 方法或過濾屬性,讓解析邏輯更靈活。

📌 實用範例:解析複合物件
我們可以組合多個解析器來解析 API 返回的資料,確保每個屬性對應的型別都符合預期。


結尾

程式世界就像是一片無限可能的宇宙,而你手中的 TypeScript 就是那架探索的飛船 🚀。
只要掌握了這些型別安全的解析技巧,無論前方有多麼複雜的資料結構,你都能自信地迎接挑戰,寫出又穩又優雅的程式碼。
不要害怕探索新知,因為每一次學習,都是你成為更厲害工程師的下一步!💪✨


上一篇
我推Day24 - TypeScript Enums 的神奇威力:讓程式碼更有條理的場景應用
下一篇
我推Day26 - 映射型別全攻略!讓你的 TypeScript 程式碼充滿智慧
系列文
我推的TypeScript 操作大全30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
harry xie
iT邦研究生 1 級 ‧ 2024-10-18 17:18:22

這裡的 Functor 就是指 functional programming 的 Functor 嗎? 有用到 map() 看起來特性有些相似

Sunny.Cat iT邦新手 3 級 ‧ 2024-10-24 12:59:09 檢舉

對的,這裡的 Functor 是參考 Functional Programming 中的概念。它允許我們透過 map() 函數,將解析器的結果轉換成不同的型別,保持程式碼的模組化和彈性。雖然 TypeScript 不是純函數式語言,但使用這種方式可以讓程式碼更安全更好維護。希望這樣能更清楚地解釋我在文章中的想法^^

harry xie iT邦研究生 1 級 ‧ 2024-10-24 17:29:01 檢舉

謝謝,我覺得很清楚,之前沒想過還可以結合這些 FP 概念

Sunny.Cat iT邦新手 3 級 ‧ 2024-10-25 12:36:28 檢舉

非常感謝你的回饋 ^^

我要留言

立即登入留言