TypeScript 常量使用技巧

挺久没写过关于 TypeScript 的文章了,这两天有新项目用到该语言,就再写一些。


ts 中,用 const 声明变量只能确保该变量的引用不会改变,但变量属性仍然可变:

const user = {
    name:'Tom',
    age:18
};

// 并不会报错
user.name = 'Jerry'


我们在 TypeScript Playground[1] 中观察上述代码生成的 .d.ts 文件,看到如下结果:

declare const user: {
    name: string;
    age: number;
};


因为 user.namestring 类型,所以重新赋值 ‘Jerry’ 并不会报错


下面分析几种常见的需求场景,以及对应的解决方法:


  1. 如果想让 user.name 不可变,怎么办?

第一个办法是,将 user.name 变成常量:

const user = {
    name: 'Tom' as 'Tom',
    age: 18
};

此时再运行上面的赋值语句就会报错:

TypeScript 常量使用技巧

user 变量的类型也变成了这样:

declare const user: {
    name: "Tom";
    age: number;
};


问题来了,如果一个对象有很多个属性都不可变,难道要像下面这样写吗:

const HTTPRequestMethod = {
  CONNECT: "CONNECT" as "CONNECT",
  DELETE: "DELETE" as "DELETE",
  GET: "GET" as "GET",
  HEAD: "HEAD" as "HEAD",
  OPTIONS: "OPTIONS" as "OPTIONS",
  PATCH: "PATCH" as "PATCH",
  POST: "POST" as "POST",
  PUT: "PUT" as "PUT",
  TRACE: "TRACE" as "TRACE"
};


程序员都不喜欢做重复的事,上面的写法其实可以简化成这样:

const HTTPRequestMethod = {
  CONNECT: "CONNECT" as const,
  DELETE: "DELETE" as const,
  ...
};

即,用 as const 替代前面的属性值以避免重复书写。

如果一个对象的全部属性都是不可变的,我们甚至可以简化成这样:

const user = {
    name: 'Tom',
    age: 18
as const;

此时,对 nameage 的改动都会报错。


  1. 怎么在声明类型时,设置属性不可变?

观察下面对象的 .d.ts 输出:

const user = {
    name:'Tom',
    age:18
as const;

得到的是:

declare const user: {
    readonly name: "Tom";
    readonly age: 18;
};

所以,要将类型的某个属性设置为不可变,加上 readonly 修饰即可。如:

type User = {
    readonly name: string;
}

const me: User = {
    name: 'Youmoo'
};


  1. 怎么复用一个已经存在的类型,让其属性变为不可变?

比如,我们想把如下类型

type User = {
    name: string;
}

变成

type User = {
    readonly name: string;
}

TypeScript 为此提供一个 Readonly<Type> 辅助类型,只需这样用:

type UserNew = Readonly<User>;

const user: UserNew = ...;


有了以上基础,我们再说一个更复杂的应用。

假设页面上有一组静态菜单(menus),每个菜单有菜单名(name)和跳转路径(link)。要求我们创建一个从菜单名到菜单的映射,怎么做?

一开始,我们的做法可能是这样的:

type Menu = {
    name: string;
    link: string
};

const menus: Menu[] = [
    {
        name: 'Home',
        link: '/',
    },
    {
        name: 'About',
        link: '/about',
    }
];

type Mapping = {
    [key: string]: Menu
}
const mappings: Mapping = menus.reduce((p, v) => {
    p[v.name] = v;
    return p;
}, {} as Mapping);

初看没什么问题,但我们发现下面代码并不会报错:

// 接着上面
// 这行代码并不会报错
const homeMenu = mappings.Homeee;

原因很简单,因为 mappings 的属性是 string 类型。但是,我们也知道,其实我们允许的属性只有 HomeAbout

怎么让 ts 检查到这种错误呢?

第一种办法比较笨,但能解决问题,就是手动列举出支持的属性:

type Menu = {
    name: string;
    link: string
};

const menus: Menu[] = [
    {
        name: 'Home',
        link: '/',
    },
    {
        name: 'About',
        link: '/about',
    }
];

type Mapping = {
    [key in 'Home' | 'About']: Menu
}
const mappings: Mapping = menus.reduce((p, v) => {
    p[v.name] = v;
    return p;
}, {} as Mapping);


// 接着上面
// 这行代码并不会报错
const homeMenu = mappings.Homeee;

观察编译提示,我们发现 ts 检测到了错误:

TypeScript 常量使用技巧

但上图中又出现了另一处错误(reduce函数内的赋值),原因是 v.namestring 类型,而 p 的属性是 ‘Home’ | ‘About’ 类型,导致类型不匹配。

怎么办,只能 workaroud 一下,对 v.name 进行类型转换:

// 注意这里
type MenuName = 'Home' | 'About';

type Mapping = {
    [key in MenuName]: Menu
}
const mappings: Mapping = menus.reduce((p, v) => {
    // 以及这里
    p[v.name as MenuName] = v;
    return p;
}, {} as Mapping);

上面说了,这种办法比较笨。一是,需要列举出所有菜单名,二是,需要类型转换。在代码编写中,凡是需要手动列举的东西,难免会出出错,因为要保证你列举的菜单名和实际菜单名的一致性。

所以有了第二种更合适的办法,复用 menus 对象中的菜单名

const menus = [
    {
        name: 'Home',
        link: '/',
    },
    {
        name: 'About',
        link: '/about',
    }
as const;// 1. 注意这里

// 2. 注意这里
type Menu = typeof menus[number];
type MenuName = typeof menus[number]['name'];

type Mapping = {
    [key in MenuName]: Menu
}
const mappings: Mapping = menus.reduce((p, v) => {
    // 不需要类型转换了
    p[v.name] = v;
    return p;
}, {} as Mapping);

通过以上代码,我们消除了 手动列举菜单名强制类型转换 两个弊端。

如果你仍然不完全理解上述代码是如何运作的,访问这个示例[2],依次将鼠标划过相关的变量和类型,看看 ts 是如何解释它们的。

另外 Mapping 这个类型还能这样写:

type Mapping = Record<MenuName, Menu>;


欢迎阅读,点个【在看】支持下吧。

参考资料

[1]

TypeScript Playground: https://www.typescriptlang.org/play

[2]

Playground 示例: https://www.typescriptlang.org/play?#code/MYewdgzgLgBAtgUzAVwjAvDA2gKBvmAbzwNLAENEAuGAcgAkRFaAaE0-AGwEswBrGrQD0rdjAC+bUsQ4EK1OgEEARiGRRRsrrwF0h5Ves0FxOALoxyaUJCgBuIUJgBGAHQxAFzaB4Q0Cb8YBnEnBxHGAAmd29-HCgATwAHBBgAWSRkDBgY+JAAM3gUiCwUOGUEACczOyi4hOSUADlKBMwMhGzclHzC4rKsWnkEWnLA5qTyWNjeAHM0mQIsPgRomF4klPrEMxoa5BxTG2h4UfGwCYhNw8m0xHbXEoQAE2RgBAAKZ9iWGAA3AEoMAD4iGJgoBYOUAAOmAQMjAN4+gGj1QA28YAjY0AYXJiWJYT6uPoWTCfCqkW5QZAlMAwWIVSREcSWNCJc7Hb4VQIgwAa2oAKdUAIW6AHeDAKrKgBh-wACRoBTuUApUaATFScHBaSdXIxEAgEEA

– END –


原文始发于微信公众号(背井):TypeScript 常量使用技巧

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/246813.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!