一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第9天,点击查看活动详情。
本文的翻译于<<Effective TypeScript>>, 特别感谢!! ps: 本文会用简洁, 易懂的语言描述原书的所有要点. 如果能看懂这文章,将节省许多阅读时间. 如果看不懂,务必给我留言, 我回去修改.
技巧15: 用索引签名来指定动态数据
在js中, 创建object非常简单, 例如有一个有任意属性名的object:
const rocket = {
name: 'Falcon 9',
variant: 'Block 5',
thrust: '7,607 kN',
};
ts可以通过用索引签名来指定这类灵活的object:
type Rocket = {[property: string]: string};
const rocket: Rocket = {
name: 'Falcon 9',
variant: 'v1.0',
thrust: '4,940 kN',
}; // OK
[property: string]: string: 就是索引签名,
它干了三件事:
- 指定了 keys 的名字: 但是这个名字不会在类型检查中起作用
- 指定了 key 的类型: string, 可以是string, number,symbol的联合体, 但是最好只用string (见 技巧16)
- 指定了values的类型: 可以是any, 但是这里是string
索引签名也存在几个问题:
- 允许任意属性名: 比如将name 写错成 Name, 也能通过类型检查.
- 允许空object: {} 也能通过类型检查.
- 无法清晰知道不同key的value的不同类型.
- 无法使用ts的便捷服务,例如自动补全功能等
总之, 索引签名不是很精确, 最好精确定义:
interface Rocket {
name: string;
variant: string;
thrust_kN: number;
}
const falconHeavy: Rocket = {
name: 'Falcon Heavy',
variant: 'v1',
thrust_kN: 15_200
};
这样就能完整享受ts完整的好处了.
那我们该何时的时候使用索引签名? 最典型的情况就是动态数据:比如从csv文件中读取数据:
function parseCSV(input: string): {[columnName: string]: string}[] {
const lines = input.split('\n');
const [header, ...rows] = lines;
return rows.map(rowStr => {
const row: {[columnName: string]: string} = {};
rowStr.split(',').forEach((cell, i) => {
row[header[i]] = cell;
});
return row;
});
}
如果使用这个parseCSV函数的人知道数据的格式, 那么可以添加断言:
interface ProductRow {
productId: string;
name: string;
price: string;
}
declare let csvData: string;
const products = parseCSV(csvData) as unknown as ProductRow[];
添加断言也不能保证数据会符合你的预期,那么我们可以给字段加上undefined类型, 这样能够增加对自己代码的检查:
function safeParseCSV(
input: string
): {[columnName: string]: string | undefined}[] {
return parseCSV(input);
}
加上之后, 每次获取数据都会要求检查:
const rows = parseCSV(csvData);
const prices: {[produt: string]: number} = {};
for (const row of rows) {
prices[row.productId] = Number(row.price);
}
const safeRows = safeParseCSV(csvData);
for (const row of safeRows) {
prices[row.productId] = Number(row.price);
// ~~~~~~~~~~~~~ Type 'undefined' cannot be used as an index type
}
当然这有可能不够精确. 比如你知道有有可能A, B, C, D四个key,但是不一定有. 那我们可以使用可选属性, 或者联合:
interface Row1 { [column: string]: number } // Too broad
interface Row2 { a: number; b?: number; c?: number; d?: number } // Better
type Row3 =
| { a: number; }
| { a: number; b: number; }
| { a: number; b: number; c: number; }
| { a: number; b: number; c: number; d: number };
如果key的类型 string太宽泛了, 比如只能从'x','y', 'z'中选取.我们可以使用泛型:
type Vec3D = Record<'x' | 'y' | 'z', number>;
// Type Vec3D = {
// x: number;
// y: number;
// z: number;
// }
当然也可以使用映射:
type Vec3D = {[k in 'x' | 'y' | 'z']: number};
// Same as above
type ABC = {[k in 'a' | 'b' | 'c']: k extends 'b' ? string : number};
// Type ABC = {
// a: number;
// b: string;
// c: number;
// }