利用空值设计让 TypeScript 更稳定和易于维护
本文代码有大量的 test 和 expect 函数,目的是替代注释,用 expect 说明变量和函数的返回值
初始化缺省数据
举一个场景的例子, 前端ajax接收后端响应数据:
interface iUser {
name:string
age:number
}
test("响应数据", function () {
let responseJSON :string = `{"name": "nimo"}`
let res:iUser = JSON.parse(responseJSON)
expect(res.name).toBe("nimo")
expect(res.age).toBe(undefined);
// iUser 中 age 不是 age?:number 但是结果值是 undefined
// 具体为什么会是 undefined,和为什么后端响应的数据没有 age 我们就不深入讨论了
// 但是这种情况会导致出现一些bug,比如:
expect(res.age + 1).toBe(NaN);
// 明明使用了ts,结果居然将 number 加上 number 得到了 NaN
// 因为此时 res.age 是 undefined
})
数据接口新增属性
接着来看对象数据的场景:
最初定义了一个数据结构,只有 url 属性
interface iPost {
url:string
}
test("然后有很多地方使用了 iData", function () {
let a :iPost = {
url: "https://github.com/nimoc/blog/issues/33"
}
console.log(a)
let b :iPost = {
url: "https://github.com/nimoc"
}
console.log(b)
})
如果此时 iPost 新增了属性 title,则会导致 a b 声明时编译期报错
interface iPost {
url:string
title: string
}
test("然后有很多地方使用了 iData", function () {
// TS2741: Property 'title' is missing in type '{ url: string; }' but required in type 'iPost'.
let a :iPost = {
url: "https://github.com/nimoc/blog/issues/33"
}
console.log(a)
// TS2741: Property 'title' is missing in type '{ url: string; }' but required in type 'iPost'.
let b :iPost = {
url: "https://github.com/nimoc"
}
console.log(b)
})
想要解决这个问题就需要在 a b 两处声明的地方加上 title 属性,如果不只是 a b 两次,而是由几十处就会变得非常麻烦.
虽然你可以认为类型系统就应该这样严格,但是在这个场景下我更希望不需要改几十处代码
如果不想改几十处可能会导致我们写出不好的代码,例如修改 iPost 为
interface iPost {
url:string
title?: string
}
这种做法虽然不需要些几十处了,但是 nimo 认为这种方式会引入不必要的 undefined . 导致明明用了类型系统,结果还要处理最繁琐的 undefined 问题.
空值设计 (zero values)
我们借鉴 golang 中zero values 的设计,来解决上述2个问题.
请看代码:
interface iPerson {
name:string
age:number
}
interface iMakePerson {
name?:string
age?:number
}
function Person(v: iMakePerson) :iPerson {
return {
name: v.name || "",
age: v.age || 0
}
}
test("响应数据空值填充", function () {
let response = Person(JSON.parse(`{"name":"nimo"}`))
// 不会出现 response.age 是 undefined 导致的 NaN 的情况
expect(response.age + 1).toBe(1)
})
test("多处使用 Person ", function () {
let a = Person({
name: "nimo",
})
expect(a).toStrictEqual({name:"nimo",age:0})
let b = Person({
age: 18,
});
expect(b).toStrictEqual({name:"",age:18})
})
如果要新增属性则只需在 iPerson 和 iPerson中分别增加新属性
比如新增了 nikename
interface iPerson {
name:string
age:number
nikename:string
}
interface iMakePerson {
name?:string
age?:number
nikename?:string
}
function Person(v: iMakePerson) :iPerson {
return {
name: v.name || "",
age: v.age || 0,
nikename: v.nikename || "",
}
}
使用所有 Person不会报错,因为接口定义了 nikename?:string
let a = Person({
name: "nimo",
})
expect(a).toStrictEqual({name:"nimo",age:0,nikename:""})
如果新增了 gender ,并且要求 gender 是必填的那么可以这样修改 iPerson
interface iPerson {
name:string
age:number
nikename:string
gender:string
}
interface iMakePerson {
name?:string
age?:number
nikename?:string
gender:string
}
function Person(v: iMakePerson) :iPerson {
return {
name: v.name || "",
age: v.age || 0,
nikename: v.nikename || "",
gender: v.gender,
}
}
注意此时在 iPerson 中 gender
不是 gender?
,没有通过 ? 定义可以为undefined. 这样在所有调用 Person 的地方都需要定义 gender
// 编译期报错
// TS2345: Argument of type '{ name: string; }' is not assignable to parameter of type 'iPerson'.
Person({
name: "nimo",
})
// 不报错
Person({
name: "nimo",
gender: "male",
})
通过空值设计可以消除代码中的 undefined , 提高开发效率,增加项目稳定性
基于空值make函数你可以略过部分属性的声明,不必要写大量的重复代码,但请切记一点在 make 函数中空值只能有
""
0
false
[]
另外一个 make 函数
这是因为如果你在 make 中定义了以上其他的值,会让调用 make 函数的人不明白到底make后属性默认值是什么.
不能用 {} 是因为 另外一个make函数替代了空值对象.
另外一个make 函数请看下面的例子
interface iSon {
name:string
}
interface iMakeSon {
name?:string
}
function Son (v :iMakeSon):iSon {
return {
name: v.name || ""
}
}
interface iFamily {
unity:boolean
son: iSon
}
interface iMakeFamily {
unity?: boolean
son?: iSon
}
function Family(v :iMakeFamily):iFamily {
return {
unity: v.unity || false,
son: v.son || Son({})
}
}
test("多层mark",function () {
let data = Family({
unity: true,
// son: Son({}) // 此行可有可无,根据实际场景决定
})
expect(data).toStrictEqual({
"son": {
"name": ""
},
"unity": true
})
})
多说一句,在 ts 中还可以将 son 直接包括在 family 中
interface iSome {
unity:boolean
son: {
name:string
}
}
使用 ts 要带入静态类型的思维,虽然会写出很多在js角度看起来很麻烦的类型代码,但是这些代码会让你的项目稳定性更高. 而空值设计可以让编写 ts 更加轻松稳定.
如果你学习 typescript 发现怎么用都不顺手,我建议先学习一门纯粹的强静态类型语言.用于掌握强静态类型语言编程思维. 因为 TypeScript 是 对 JavaScript进行 类型批注,而不是真正意义上的静态类型语言.
如果你觉得空值函数的设计不错,请将本文推荐给你的朋友或同事
这样能让更多人提供更安全的make函数.
在Github发表评论: https://github.com/nimoc/fe/discussions/59