功能设计:智能构造函数

2025-06-05

功能设计:智能构造函数

有时,你需要对程序中的值进行一些超出常规类型系统检查范围的保证。智能构造函数可以用于此目的。

问题

interface Person {
  name: string
  age: number
}

function person(name: string, age: number): Person {
  return { name, age }
}

const p = person('', -1.2) // no error
Enter fullscreen mode Exit fullscreen mode

如你所见,stringnumber是广义类型。我该如何定义非空字符串?或者正数?或者整数?或者正整数?

更一般地:

我如何定义类型的细化T

 食谱

  1. 定义一个R代表细化的类型
  2. 不要导出构造函数R
  3. 导出具有以下签名的函数(智能构造函数)
make: (t: T) => Option<R>
Enter fullscreen mode Exit fullscreen mode

可能的实现方式:品牌类型

品牌类型T独特品牌相交的类型

type BrandedT = T & Brand
Enter fullscreen mode Exit fullscreen mode

让我们NonEmptyString按照上面的方法来实现:

  1. 定义一个NonEmptyString代表细化的类型
export interface NonEmptyStringBrand {
  readonly NonEmptyString: unique symbol // ensures uniqueness across modules / packages
}

export type NonEmptyString = string & NonEmptyStringBrand
Enter fullscreen mode Exit fullscreen mode
  1. 不要导出构造函数NonEmptyString
// DON'T do this
export function nonEmptyString(s: string): NonEmptyString { ... }
Enter fullscreen mode Exit fullscreen mode
  1. 导出智能构造函数make: (s: string) => Option<NonEmptyString>
import { Option, none, some } from 'fp-ts/Option'

// runtime check implemented as a custom type guard
function isNonEmptyString(s: string): s is NonEmptyString {
  return s.length > 0
}

export function makeNonEmptyString(s: string): Option<NonEmptyString> {
  return isNonEmptyString(s) ? some(s) : none
}
Enter fullscreen mode Exit fullscreen mode

age让我们为这个领域做同样的事情

export interface IntBrand {
  readonly Int: unique symbol
}

export type Int = number & IntBrand

function isInt(n: number): n is Int {
  return Number.isInteger(n) && n >= 0
}

export function makeInt(n: number): Option<Int> {
  return isInt(n) ? some(n) : none
}
Enter fullscreen mode Exit fullscreen mode

用法

interface Person {
  name: NonEmptyString
  age: Int
}

function person(name: NonEmptyString, age: Int): Person {
  return { name, age }
}

person('', -1.2) // static error

const goodName = makeNonEmptyString('Giulio')
const badName = makeNonEmptyString('')
const goodAge = makeInt(45)
const badAge = makeInt(-1.2)

import { option } from 'fp-ts/Option'

option.chain(goodName, name => option.map(goodAge, age => person(name, age))) // some({ "name": "Giulio", "age": 45 })

option.chain(badName, name => option.map(goodAge, age => person(name, age))) // none

option.chain(goodName, name => option.map(badAge, age => person(name, age))) // none
Enter fullscreen mode Exit fullscreen mode

结论

这似乎只是将运行时检查的负担推给了调用者。这很合理,但调用者反过来可能会把这个负担推给它的调用者,如此反复,直到到达系统边界,而此时你无论如何都应该进行输入验证。

对于可以轻松在系统边界进行运行时验证并支持品牌类型的库,请查看io-ts

文章来源:https://dev.to/gcanti/functions-design-smart-constructors-14nb
PREV
fp-ts 入门:Either 与 Validation 问题 Either Validation 附录
NEXT
为什么我更喜欢 Vue 而不是 React 结论