Lock in $30 Savings on PRO—Offer Ends Soon! ⏳

從 Functional Programming 的角度看 2021 的 TypeScript

Jerry Hong
September 23, 2021

從 Functional Programming 的角度看 2021 的 TypeScript

如今的 Typescript 相較剛出來時已經熟成許多,現在是否真的值得在開發上使用 Typescript?從 Functional Programming 的角度來說 Typescript 有什麼缺陷?使用 Typescript 需要注意哪些問題?使用 Typescript 能帶來什麼優勢?什麼情況下適合導入 Typescript?

Jerry Hong

September 23, 2021
Tweet

More Decks by Jerry Hong

Other Decks in Programming

Transcript

  1. type A = 1 type B = 1 | 2

    | 3 type C = 1 | 2 | 3 | ... // write all number // C = number type D = '' | 'a' | 'b' | ... // write all string // D = string type E = true | false // E = boolean Type 就像是集合 (Set) TS
  2. const add1 = x => 1 + x add1(2) //

    3 ⽤ JS 定義⼀個 Add1 function JS add1('abc') // '1abc' type Add1 = (x: number) => number
  3. const add1 = x => 1 + x add1(2) //

    3 定義不明 add1('abc') // '1abc' type Add1 = (x: number) => number JS
  4. Type 幫助我們定 義 function TS const add1: Add1 = x

    => 1 + x add1(2) // 3 add1('abc') // Type Error type Add1 = (x: number) => number
  5. Jerry Hong Tech Leader | Website: blog.jerry-hong.com Branch8 2019 ModernWeb

    speaker 2018 FEDC organiser 2017 JSDC.tw speaker 2017 RxJS 30天鐵⼈賽冠軍 2016 JSDC.tw speaker
  6. Type 帶來的優勢 |> 型別檢查 (type check) 可以在 編譯時期 (compile time)

    檢測出很多 潛在的 bugs |> 型別 (type) 能提供最新的⽂件 (up-to-date document) |> 型別 (type) 可以驅動開發 (Type-Driven Development)
  7. TypeScript Release History |> 1.4 - Union Type, Type Guard,

    Type Aliases |> 1.6 - Intersection Type, User-defined Type Guard Function |> 1.8 - Constraints Generics |> 2.0 - undefined(null)-aware Types, Control Flow Based Type Analysis, Discriminated Union Types, never type |> 2.1 - keyof and lookup types, mapped types |> 2.8 - Conditional Types |> 4.0 - Variadic Tuple Types, Labeled Tuple Elements |> 4.1 - Template Literal Types, Key Remapping in Mapped Types, Recursive Conditional Types |> 4.4 - Control Flow Analysis of Aliased Conditions and Discriminants
  8. VS Code |> ⽬前最多⼈使⽤的 Editor |> 原⽣⽀援 TypeScript |> Go

    to definition |> Auto-complete |> Refactoring |> Renaming |> ...
  9. TypeScript 的優點 |> 許多 JS Library 採⽤ TypeScript |> Editor

    ⽀援良好 |> 前後端可共⽤, 且⽣態⽀援好
  10. 前後端可共⽤, 且⽣態⽀援好 |> 前後端可共⽤ Type 甚⾄ Validation |> Library 及

    Tools |> prisma |> typeorm |> apollo-server |> graphql-code-generator |> zod |> ts-json-validator
  11. TypeScript 的優點 |> 許多 JS Library 採⽤ TypeScript |> Editor

    ⽀援良好 |> 前後端可共⽤, 且⽣態⽀援好 |> Type system 圖靈完備 (Turing Completeness)
  12. Type system 圖靈完備 (Turing Completeness) | > 可以⽤ Type System

    做任何運算 | > 可以做 Type Level Programming | > 定義⾃然數 | > 可以做到⼀些 Dependent type 的效果 | > Type check is undecidable | > Implement Collatz | > The Undecidability of the Generalized Collatz Problem
  13. TypeScript 的優點 |> 許多 JS Library 採⽤ TypeScript |> Editor

    ⽀援良好 |> 前後端可共⽤, 且⽣態⽀援好 |> Type system 圖靈完備 (Turing Completeness) |> 在非嚴格模式及可⽤ Any 的狀況下, 對 JS 的開發者來說相對好上⼿
  14. Third Party Library Issue |> Type 定義錯誤 |> 仍有許多 Library

    是由 JS 撰寫, 只有維護⼀份型別定義檔 (d.ts), 可 能存在 Type 定義錯誤 |> TypeScript 本⾝的 unsound, 非嚴格模式 以及 any 被濫⽤的可能, 導 致 Type Check 若有似無 |> Config 設定不⼀致, 導致奇怪的 Bug |> 嚴格模式下 xstate 的 assign
  15. TypeScript 的缺點 |> 不成熟 |> Third Party Library Issue |>

    沒有真正的 Tagged Union Type |> 沒有 Pattern Match
  16. type Circle = { kind: 'circle' radius: number } type

    Square = { kind: 'square' side: number } type Shape = Circle | Square Tagged Union Type |> Sum type |> 必須加上識別屬性 TS
  17. type Circle = { kind: 'circle' radius: number } type

    Square = { kind: 'square' side: number } type Shape = Circle | Square Tagged Union Type |> Sum type |> 必須加上識別屬性 |> 需要透過屬性判定, 來完成 type guard |> 沒有 Pattern Match TS function f(s: Shape) { if (s.kind === 'circle') { return s.radius } if (s.kind === 'square') { return s.side } }
  18. TS type Circle = { kind: 'circle', radius: number }

    type Square = { kind: 'square', side: number } type Shape = Circle | Square function f(s: Shape) { if (s.kind === 'circle') { return s.radius } if (s.kind === 'square') { return s.side } } data Shape = Circle { radius :: Float } | Square { side :: Float } f :: Shape -> Float f Circle { radius } = radius f Square { side } = side
  19. TS type Circle = { kind: 'circle', radius: number }

    type Square = { kind: 'square', side: number } type Shape = Circle | Square function f(s: Shape) { if (s.kind === 'circle') { return s.radius } if (s.kind === 'square') { return s.side } } type Shape = | Circle of radius: float | Square of side: float let f shape = match shape with | Circle(radius = r) -> r | Square(side = s) -> s
  20. TypeScript 的缺點 |> 不成熟 |> Third Party Library Issue |>

    沒有真正的 Tagged Union Type |> 沒有 Pattern Match |> 沒有 New Type
  21. type email = string type FindUser = (email: email) =>

    User const findUser: FindUser = (email) => {} findUser('www') // no error 沒有 New Type |> 只有 type alias TS
  22. TypeScript 的缺點 |> 不成熟 |> Third Party Library Issue |>

    沒有真正的 Tagged Union Type |> 沒有 Pattern Match |> 沒有 New Type |> 難以理解的 Type Error Message
  23. type A = string | number function f(a: A) {}

    f(false) 難以理解的 Type Error Message TS
  24. TypeScript 的缺點 |> 不成熟 |> Third party library issue |>

    沒有真正的 Tagged Union Type |> 沒有 Pattern Match |> 沒有 New Type |> 難以理解的 Type Error Message |> 缺陷 |> Unsoundness
  25. let x: number | null = 12 function nullX() {

    x = null } nullX() x.toFixed(1) // no error Unsoundness TS ⽬前還有 72 個 open unsound issue 在 github
  26. TypeScript 的缺點 |> 不成熟 |> Third Party Library Issue |>

    沒有真正的 Tagged Union Type |> 沒有 Pattern Match |> 沒有 New Type |> 難以理解的 Type Error Message |> 缺陷 |> Unsoundness |> Syntax 混亂
  27. export function createMachine< TContext, TEvent extends EventObject = AnyEventObject, TTypestate

    extends Typestate<TContext> = { value: any; context: TContext } >( config: MachineConfig<TContext, any, TEvent>, options?: Partial<MachineOptions<TContext, TEvent >> ): StateMachine<TContext, any, TEvent, TTypestate> { return new StateNode<TContext, any, TEvent, TTypestate>( config, options ) as StateMachine<TContext, any, TEvent, TTypestate>; } Syntax 混亂 |> 容易把 Type 跟 Code 混在⼀起 TS
  28. type Diff<T, U> = T extends U ? never :

    T; // Remove types from T that are assignable to U type Filter<T, U> = T extends U ? T : never; // Remove types from T that are not assignable to U type Result1 = Diff<'1' | '2' | '3', '3'> // '1' | '2' type Result2 = Filter<'1' | '2' | '3', '3'> // '3' type UnionToIntersection<U> = ( U extends never ? never : (arg: U) => never ) extends (arg: infer I) => void ? I : never; type UnionToTuple<T> = UnionToIntersection< T extends never ? never : (t: T) => T > extends (_: never) => infer W ? [ ... UnionToTuple<Exclude<T, W >> , W] : []; type A = UnionToTuple<'1' | '2' | '3'> // ['1', '2', '3'] Syntax 混亂 |> 容易把 Type 跟 Code 混在⼀起 |> Type syntax 可讀 性極差 TS
  29. TypeScript 的缺點 |> 不成熟 |> Third party library issue |>

    沒有真正的 Tagged Union Type |> 沒有 Pattern Match |> 沒有 New Type |> 難以理解的 Type Error Message |> 缺陷 |> Unsoundness |> Syntax 混亂 |> Type Infer 很差
  30. // map :: (a -> b) -> Array a ->

    Array b type CurriedMapFn = <T, N>(fn: (e: T) => N) => (x: T[]) => N[] const curriedMap:CurriedMapFn = (fn) => (arr) => arr.map(fn) type MapFn = <T, N>(fn: (e: T) => N, x: T[]) => N[] const map:MapFn = (fn, arr) => arr.map(fn) const result2 = map((a) => a + 1, [1,2,3]) const result = curriedMap((a) => a + 1)([1,2,3]) // Type Error, a is unknown Curry function 的 type inference 很差 TS #45438
  31. type primitiveType = 'string' | 'number' | 'boolean' const a

    = (p: primitiveType) => { if (p === 'string') { p // type is 'string' } } const b = <T extends primitiveType>(p: T) => { if (p === 'string') { p // type is still primitiveType } } Union type 的 type guard 在泛型中無 效 TS #36772
  32. TypeScript 的缺點 |> 不成熟 |> Third party library issue |>

    沒有真正的 Tagged Union Type |> 沒有 Pattern Match |> 沒有 New Type |> 難以理解的 Type Error Message |> 缺陷 |> Unsoundness |> Syntax 混亂 |> Type Infer 很差 |> Error 不是 First Class
  33. let a: Promise<{ name: string, age: number }> a.then(r =>

    r.name) .catch(error => {}) Promise 無法定義 rejection type TS #39680
  34. TypeScript 的缺點 | > 不成熟 | > Third party library

    issue | > 沒有真正的 Tagged Union Type | > 沒有 Pattern Match | > 沒有 New Type | > 難以理解的 Type Error Message | > 缺陷 | > Unsoundness | > Syntax 混亂 | > Type Infer 很差 | > Error 不是 First Class | > Compiler 效能差
  35. Compiler 效能差 |> TypeScript 的 Compiler 是⽤ TypeScript 寫的 (跑在

    Node.js) |> ⼤型專案 Type Check 會很慢 |> 電腦不能太差 |> TypeScript 團隊不打算⽤ Rust 重寫 Compiler
  36. Compiler 效能差 |> TypeScript 的 Compiler 是⽤ TypeScript 寫的 (跑在

    Node.js) |> ⼤型專案 Type Check 會很慢 |> 電腦不能太差 |> TypeScript 團隊不打算⽤ Rust 重寫 Compiler |> ⽬前⽤別的語⾔重寫的 TypeScript Transpiler |> swc |> esbuild |> SWC 的作者正在⽤ Rust 重寫 TS Type Checker
  37. 專案該不該採⽤ TypeScript ? | > 採⽤或導入任何技術 | > 明確知道該技術的優缺點 |

    > 相關替代技術的橫向對比 | > 確認團隊成員的接受度 | > 團隊是否有⾜夠熟悉該技術的⼈ | > 未來是否好招⼈ | > 專案是否有⾜夠的資源做技術的轉換 | > 建議 | > 團隊中⾄少有⼀名以上的開發者熟悉 TypeScript | > 知道 generic type 的使⽤⽅式 | > 知道如何操作 type | > 知道 TypeScript 的地雷 | > 有良好的開發流程 - Code Review
  38. /* Strict Type-Checking Options */ "strict": true, /* Enable all

    strict type-checking options. */ "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ "strictNullChecks": true, /* Enable strict null checks. */ "strictFunctionTypes": true, /* Enable strict checking of function types. */ "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ TSConfig TS |> ⾄少要開 strictNullChecks |> 開發 Library 建議 直接 strict
  39. 開發⼿法 TS |> 記得 TS 的 Type Check 很笨 |>

    盡可能保持 Function 是 Pure |> 盡可能使⽤ immutable 的⽅式操 作資料 |> 留意任何 mutable 的 操作 |> 可以考慮採⽤
 eslint-plugin- immutable type A = { name: string, account: number } let a: A = { name: 'jerry', account: 123456 } type B = { name: string, account: number | string } let b: B = a b.account = '123' a.account.toFixed(0) // Runtime error
  40. 開發⼿法 TS |> 記得 TS 的 Type Check 很笨 |>

    盡可能保持 Function 是 Pure |> 盡可能使⽤ immutable 的⽅式操 作資料 |> 留意任何 mutable 的 操作 |> 可以考慮採⽤
 eslint-plugin- immutable type A = { name: string, account: number } const a: A = { name: 'jerry', account: 123456 } type B = { name: string, account: number | string } const b: B = { ... a, account: '123' } a.account.toFixed(0) // Ok
  41. const getUserName = (user: { firstName: string, lastName: string, age:

    number, phone: string }): string => { return `${user.firstName} ${user.lastName}` } 不要把 Type 跟 Code 混在⼀起 TS
  42. type User = { firstName: string, lastName: string, age: number,

    phone: string } type GetUserName = (user: User) => string const getUserName: GetUserName = (user) => { return `${user.firstName} ${user.lastName}` } 獨立寫 Type TS
  43. type User = { firstName: string, lastName: string, age: number,

    phone: string } type GetUserName = (user: User) => string const getUserName: GetUserName = (user) => { return `${user.firstName} ${user.lastName}` } getUserName({ firstName: 'Jerry', lastName: 'Hong' }) // Type Error 確保 Type 正確性 TS
  44. type User = { firstName: string, lastName: string, age: number,

    phone: string } type GetUserName = (user: { firstName: string, lastName: string }) => string const getUserName: GetUserName = (user) => { return `${user.firstName} ${user.lastName}` } getUserName({ firstName: 'Jerry', lastName: 'Hong' }) // Ok 不要重複寫 Type TS
  45. type User = { firstName: string, lastName: string, age: number,

    phone: string } type GetUserName = (user: Pick<User, 'firstName' | 'lastName'>) => string const getUserName: GetUserName = (user) => { return `${user.firstName} ${user.lastName}` } getUserName({ firstName: 'Jerry', lastName: 'Hong' }) // Ok 不要重複寫 Type TS
  46. type UncapitalizeKeys<T extends object> = Uncapitalize<keyof T & string>; type

    UncapitalizeObjectKeys<T extends object> = { [key in UncapitalizeKeys<T>]: Capitalize<key> extends keyof T ? T[Capitalize<key>] : never; } type A = UncapitalizeObjectKeys<{ FooHa: 'abc', Bar: 'def'}> TS 善⽤ type utility |> 熟悉官⽅提供的 Utility Types |> 善⽤第三⽅的 
 Type Utilities |> type-fest |> utility-types |> ts-toolbelt
  47. import { CamelCasedPropertiesDeep } from 'type-fest'; type A = CamelCasedPropertiesDeep<{

    FooHa: 'abc', Bar: 'def' }> TS 善⽤ type utility |> 熟悉官⽅提供的 Utility Types |> 善⽤第三⽅的 
 Type Utilities |> type-fest |> utility-types |> ts-toolbelt
  48. type UserInput = { name: string account: string password: string

    } type CreateUser = (input: UserInput) => Promise<User> const createUser: CreateUser = (input) => { // ... } app.get('/create-user', async (req, res) => { const userInput = req.body as UserInput const user = await createUser(userInput) }) 善⽤ Validator TS |> 永遠記得 TS 不保證 type safe |> 永遠不要相信從外部 世界來的 value |> Library |> zod |> ts-json-validator
  49. import { z } from 'zod'; const UserInputSchema = z.object({

    name: z.string().min(3).max(50), account: z.string().email(), password: z.string().min(7).max(50) }) type UserInput = z.infer<typeof UserInputSchema> type CreateUser = (input: UserInput) => Promise<User> const createUser: CreateUser = (input) => { // ... } app.get('/create-user', async (req, res) => { const userInput = await UserInputSchema.safeParseAsync(req.body) if (userInput.success) { const user = await createUser(userInput.data) } }) 善⽤ Validator TS |> 永遠記得 TS 不保證 type safe |> 永遠不要相信從外部 世界來的 value |> Library |> zod |> ts-json-validator
  50. import { Either, isRight } from 'fp-ts/Either'; type CreateUser =

    (input: UserInput) => Promise<Either<Error, User >> const createUser: CreateUser = (input) => { // ... } app.get('/create-user', async (req, res) => { const userInput = await UserInputSchema.safeParseAsync(req.body) if (userInput.success) { const userE = await createUser(userInput.data) if (isRight(userE)) { userE.right // User } else { userE.left // Error } } }) 進階: 處理 Error Type TS
  51. 良好的開發流程 |> 專⼈負責 Type |> Type ⼀致性 |> 檢查是否有 any

    |> 檢查 mutable 操作 |> 檢查外部資料是否 有驗證 |> Code Review |> CI - type check