问题
之前在做网站本地化时, 在类型方面我们直接使用 typeof, 类似这样:
/// type.d.ts
import localeObj from 'file-path/to/locale'
export type Locale = typeof localeObj;
但是这样导出的类型只包含key, 具体的值都变成了string, 类似这样:
{
foo: string;
bar: {
foo_bar: string;
bar_foo: string;
}
}
开始也就这么用了, 但是后面在阅读代码的时候, 一旦遇到本地化内容, 编辑器是没有办法提示我, 这个内容具体是什么的, 有些key定义的比较好的还能看出来, 但更多的时候需要跳转到本地化文件才能看到具体内容。 这时我想到的第一个方法就是赋值每个本地化对象, 重新导出为类型:
export const locale = {
foo: 'foo';
bar: {
foo_bar: 'foo_bar';
bar_foo: 'bar_foo';
}
}
export type Locale = {
foo: 'foo';
bar: {
foo_bar: 'foo_bar';
bar_foo: 'bar_foo';
}
}
但...这不是蛋疼么...
一通尝试后, 我们可以使用一个叫做Const 断言(const assertions)的特性, 来省略掉上面哪个蛋疼类型了:
export const locale = {
foo: 'foo';
bar: {
foo_bar: 'foo_bar';
bar_foo: 'bar_foo';
}
} as const
Const 断言
说实话, 这东西要是不知道, 也确实不好搜, 我是之前有点印象, 大概是在TS的哪个版本引入了一个新特性什么的, 当时光看更新日志其实不是很明白具体是做什么的, 这会遇到问题了试一试才终于明白。
这个特性是在TS的3.4版本引入的, 被定义为一个字面量的构造器, 属于类型断言, 具体语法是在值后面断言为const:
// Type '"hello"'
let x = "hello" as const;
// Type 'readonly [10, 20]'
let y = [10, 20] as const;
// Type '{ readonly text: "hello" }'
let z = { text: "hello" } as const;
使用这个断言后, TS就明白:
- 此表达式中的任何字面类型不应被拓宽
- 对象字面量会获得
readonly属性 - 数组字面量会变成
readonly元组
通过这个类型断言, 就可以省略一些只用于提示编译器不变性的类型(就比如我们上面的例子)
注意事项
const断言只能用于简单的字面量表达式
// Error! A 'const' assertion can only be applied to a
// to a string, number, boolean, array, or object literal.
let a = (Math.random() < 0.5 ? 0 : 1) as const;
let b = (60 * 60 * 1000) as const;
// Works!
let c = Math.random() < 0.5 ? (0 as const) : (1 as const);
let d = 3_600_000 as const;
另外, const断言不能完全的把表达式转为不可变的
let arr = [1, 2, 3, 4];
let foo = {
name: "foo",
contents: arr,
} as const;
foo.name = "bar"; // error!
foo.contents = []; // error!
foo.contents.push(5); // ...works!
总结
总的来说, const 断言会让编译器推断一个表达式的类型时尽量收窄, 从而得到更精确的类型定义, 而默认行为则可能会用更宽, 更泛化的类型。 更精确的类型可以让编译器更加了解代码的意图, 也能更准确的区分代码是正确的, 还是有bug的。