功能设计:智能构造函数
有时,你需要对程序中的值进行一些超出常规类型系统检查范围的保证。智能构造函数可以用于此目的。
问题
interface Person {
name: string
age: number
}
function person(name: string, age: number): Person {
return { name, age }
}
const p = person('', -1.2) // no error
如你所见,string
和number
是广义类型。我该如何定义非空字符串?或者正数?或者整数?或者正整数?
更一般地:
我如何定义类型的细化
T
?
食谱
- 定义一个
R
代表细化的类型 - 不要导出构造函数
R
- 导出具有以下签名的函数(智能构造函数)
make: (t: T) => Option<R>
可能的实现方式:品牌类型
品牌类型是T
与独特品牌相交的类型
type BrandedT = T & Brand
让我们NonEmptyString
按照上面的方法来实现:
- 定义一个
NonEmptyString
代表细化的类型
export interface NonEmptyStringBrand {
readonly NonEmptyString: unique symbol // ensures uniqueness across modules / packages
}
export type NonEmptyString = string & NonEmptyStringBrand
- 不要导出构造函数
NonEmptyString
// DON'T do this
export function nonEmptyString(s: string): NonEmptyString { ... }
- 导出智能构造函数
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
}
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
}
用法
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
结论
这似乎只是将运行时检查的负担推给了调用者。这很合理,但调用者反过来可能会把这个负担推给它的调用者,如此反复,直到到达系统边界,而此时你无论如何都应该进行输入验证。
对于可以轻松在系统边界进行运行时验证并支持品牌类型的库,请查看io-ts
文章来源:https://dev.to/gcanti/functions-design-smart-constructors-14nb