[译]<<Effective TypeScript>> 高效TypeScript62个技巧 技巧15

211 阅读3分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 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: 就是索引签名,

它干了三件事:

  1. 指定了 keys 的名字: 但是这个名字不会在类型检查中起作用
  2. 指定了 key 的类型: string, 可以是string, number,symbol的联合体, 但是最好只用string (见 技巧16)
  3. 指定了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;
// }