利用空值设计让 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 函数。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论