原文地址:https://medium.com/@islizeqiang/why-are-const-assertions-a-gem-in-typescript-e1d353f5d8ce
本文将介绍 TypeScript const
断言。使用const
断言可以减少很多笨重的类型声明。
首先,我们要来看一下,什么是const
断言。
我们从 TypeScript 的官方文档中摘出来:
TypeScript 3.4 introduces a new construct for literal values called const
assertions. Its syntax is a type assertion with const
in place of the type name (e.g. 123 as const
). When we construct new literal expressions with const
assertions, we can signal to the language that
- no literal types in that expression should be widened (e.g. no going from
"hello"
tostring
) - object literals get
readonly
properties - array literals become
readonly
tuples
接下来,我们会一条一条解释。
no literal types in that expression should be widened
表达式中的字面量类型不会被放宽
首先,我们要知道,什么是字面量类型的放宽:
let method = 'GET';
我们使用了let
关键字声明变量。
此时,method
类型推断为string
。这意味着,所有 string 类型的值都可以赋值给method
:
let method = 'GET'; method = 'POST';
这可能不是我们想要的,毕竟,method
只能是GET
。于是,我们把method
换用const
声明:
const method = 'GET';
现在,method
类型是"GET"
。这意味着,method
的类型被收窄到了"Get"
。此时,你不能给method
赋别的什么值。这与 JavaScript 的规则是一致的:使用let
声明的变量意味着以后可能会被改变,使用const
声明的变量则不允许改变其值。
所谓字面量类型就是能够精确描述一个值的类型,例如一个指定的字符串(例如上面的"GET"
),一个指定的数字、boolean
值或者一个具体的枚举值。
const
断言帮助我们达到这一目的,生成不可扩展的字面量类型。这意味着这种类型不能被放宽到更一般的类型:
现在,method
并没有被扩展到string
类型。当然,这只是一个简单的示例,其效果与const
声明相同。如果你真的是要声明一个常量,应该使用const
声明,而不是const
断言。
那么,const
断言可以用来干什么?
比如,有这么一个函数声明:
const request = (url: string, method: 'GET' | 'POST') => fetch(url, { method });
同时,我们有一个映射表,来保存method
常量:
为什么会报错?因为 TypeScript 会把METHOD_MAP
的类型推断为:
所以,为什么METHOD_MAP.GET
的类型被推断为string
,而不是"GET"
?事实上,我们的确使用了const
去声明METHOD_MAP
。我们看另外一个例子:
// { name: string; age: number; } const person = { name: '1', age: 2, }; // OK person.age = 3;
这个例子显示出,TypeScript 并没有针对类型做进一步的推断,从而因此保证了类型的灵活性。试想,如果在这个例子中,person
类型被推断为{ name: '1', age: 2 }
,那么,我们就不得不为每一种person
创建一个类型了。
在这个时候,我们就可以使用const
断言声明METHOD_MAP
的类型:
Object literal gets read-only property
对象字面量获得只读属性
给每一个属性添加const
断言的确太麻烦了,我们可以给整个对象添加const
断言。
这种语法同时添加了readonly
修饰符。
另外,我们还可以使用尖括号语法:
const METHOD_MAP = <const>{ GET: 'GET', POST: 'POST', }; // Cannot assign to 'GET' because it is a read-only property METHOD_MAP.GET = "GET"
给整个对象添加尖括号语法和对单个属性添加是有区别的:
array literals become readonly
tuples
数组字面量变成readonly
元组
这很容易理解:
现在,我们简单介绍过 TypeScript 的const
断言。
总的来说,const
断言允许 TypeScript 推断表达式中更具体的类型。在使用时,我们还需要注意一点,表达式语句。
例如,const
断言只能放在简单字面量表达式后面:
// 错误!const 断言只能被应用到字符串、数字、布尔、数组或对象字面量 let a = (Math.random() < 0.5 ? 0 : 1) as const; let b = (60 * 60 * 1000) as const; // 成功! let c = Math.random() < 0.5 ? (0 as const) : (1 as const); let d = 3_600_000 as const;
另外,这种常量上下文并不会直接将表达式转换为完全不可变的。例如,如果某个属性又是一个数组,那么,这个属性数组并不会转变为不可变类型。
let arr = [1, 2, 3, 4]; let foo = { name: 'foo', contents: arr, } as const; // 错误! foo.name = 'bar'; // 错误! foo.contents = []; foo.contents.push(5); // 成功!
总结
在上面的METHOD_MAP
的例子中,const
断言并不是必须的。我们也可以使用枚举:
设计枚举的目的就是描述常量,给常量一个名字。所以,枚举的成员就是只读的。因此,字符串枚举的成员就是字符串字面量类型。某些开发人员不喜欢使用枚举,因为枚举不是传统的 JavaScript。此时,const
断言可以作为一个替代品。
const
断言允许我们编写更少的类型声明。如果你使用 Redux,const
断言能够极大地减少 Action、Reducer 以及类型守卫。例如:
let test: { readonly a: 'a'; readonly b: { readonly name: 1; }; readonly c: readonly [true, false]; } = { a: 'a', b: { name: 1 }, c: [true, false], };