TypeScript中的const断言
背景
最近在使用 ant design 的 Table 组件,遇到一个问题,定义 Table 的 columns 属性如下:
const columns: ColumnProps<MyRecord>[] = [{
title: 'ID',
dataIndex: 'id',
align: 'center',
}]
此时 TS 提示:
Types of property 'align' are incompatible.
Type 'string' is not assignable to type '"right" | "left" | "center" | undefined'. TS2322
问题分析
经过查看代码发现, ColumnProps 的 align 属性的实际继承自 ColumnSharedType:
interface ColumnSharedType<RecordType> {
// ...
align?: AlignType;
}
export declare type AlignType = 'left' | 'center' | 'right';
也就是 column 中的 align 的类型为 string,而 string 无法 cast 到 AlignType。即:
type AlignType = 'left' | 'center' | 'right';
let align: AlignType | undefined
let myAlign = "center"
// Type 'string' is not assignable to type '"left" | "right" | "center" | undefined'.ts(2322)
align = myAlign
解决方案
TypeScript 3.4 版本以上提供了 const 断言,可以通过 as const 来消除以上报错:
const columns: ColumnProps<MyRecord>[] = [{
title: 'ID',
dataIndex: 'id',
align: 'center' as const,
}]
const assertions
以下内容翻译自 https://devblogs.microsoft.com/typescript/announcing-typescript-3-4-rc/ 中的 const assertions。(翻译不动的地方会贴上原文)
当声明一个可变的变量或者属性时,TypeScript 经常拓宽值以确保我们可以在不写明确切的类型的情况下进行赋值。
let x = "hello";
// 我们可以给 x 赋值
x = "world";
严格来说,每一个字面量都有其字面类型。上面的 x 在类型推断时,由 "hello" 类型转为更为 widen 的 string 类型。
相对而言,如果 x 原始字面量类型为 “hello”, 那么我们就无法将 x 赋值为 “world”:
let x: "hello" = "hello";
// error!
x = "world";
上面的情况看起来比较极端,但是在一些情况下比较有用的,例如,我们经常创建一个联合属性的对象:
type Shape =
| { kind: "circle", radius: number }
| { kind: "square", sideLength: number }
function getShapes(): readonly Shape[] {
let result = [
{ kind: "circle", radius: 100, },
{ kind: "square", sideLength: 50, },
];
// Some terrible error message because TypeScript inferred
// 'kind' to have the type 'string' instead of
// either '"circle"' or '"square"'.
return result;
}
可修改性是 TypeScript 断定什么时候 widen 类型的重要因素,而非分析所有代码。(Mutability is one of the best heuristics of intent which TypeScript can use to determine when to widen (rather than analyzing our entire program).)
正如上述例子,不幸的是,JavaScript 中的属性默认可修改。这意味着经常会有不必要的 widen 类型,我们需要在特定的地方对类型进行确切的声明:
function getShapes(): readonly Shape[] {
// This explicit annotation gives a hint
// to avoid widening in the first place.
let result: readonly Shape[] = [
{ kind: "circle", radius: 100, },
{ kind: "square", sideLength: 50, },
];
return result;
}
上述的方法可以解决问题,但是当数据结构越来越复杂的时候,这种方式将会显的笨重。
为了解决这个问题,TypeScript 3.4 为字面值引入了 const 断言。它的语法是一种类型断言,例如 123 as const。当我们这么使用的时候,意味着:
- 字面量类型不会被 widen,如不会从 “hello” 变成 string
- 对象字面量的属性值为只读
- 数组字面量变成只读元组
// Type '10'
let x = 10 as const;
// Type 'readonly [10, 20]'
let y = [10, 20] as const;
// Type '{ readonly text: "hello" }'
let z = { text: "hello" } as const;
在 tsx 文件以外,还可以这么使用:
// Type '10'
let x = <const>10;
// Type 'readonly [10, 20]'
let y = <const>[10, 20];
// Type '{ readonly text: "hello" }'
let z = <const>{ text: "hello" };
This feature often means that types that would otherwise be used just to hint immutability to the compiler can often be omitted.
// Works with no types referenced or declared.
// We only needed a single const assertion.
function getShapes() {
let result = [
{ kind: "circle", radius: 100, },
{ kind: "square", sideLength: 50, },
] as const;
return result;
}
for (const shape of getShapes()) {
// Narrows perfectly!
if (shape.kind === "circle") {
console.log("Circle radius", shape.radius);
}
else {
console.log("Square side length", shape.sideLength);
}
}
注意上面无需类型声明,const 断言允许 ts 使用最精确的表达式类型。
警告
需要注意的是,const 断言只能使用在简单的字面表达式:
// Error!
// A 'const' assertion can only be applied to a string, number, boolean, array, or object literal.
let a = (Math.random() < 0.5 ? 0 : 1) as const;
// Works!
let b = Math.random() < 0.5 ?
0 as const :
1 as const;
另外需要注意的是,const 不会将表达式转变为完全不可变:
et arr = [1, 2, 3, 4];
let foo = {
name: "foo",
contents: arr,
};
foo.name = "bar"; // error!
foo.contents = []; // error!
foo.contents.push(5); // ...works!