利用空值设计让 TypeScript 更稳定和易于维护

发布于 2024-09-29 04:54:28 字数 5633 浏览 6 评论 0

本文代码有大量的 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 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

沙与沫

暂无简介

0 文章
0 评论
430 人气
更多

推荐作者

新人笑

文章 0 评论 0

mb_vYjKhcd3

文章 0 评论 0

小高

文章 0 评论 0

来日方长

文章 0 评论 0

哄哄

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文