[译]<<Effective TypeScript>> 技巧27 使用函数构造和库来帮助类型流动

454 阅读3分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第19天,点击查看活动详情

本文的翻译于<<Effective TypeScript>>, 特别感谢!! ps: 本文会用简洁, 易懂的语言描述原书的所有要点. 如果能看懂这文章,将节省许多阅读时间. 如果看不懂,务必给我留言, 我回去修改.

技巧27: 使用函数构造和库来帮助类型流动

在python, C, java中有很多好用的内置库, 但是js的内置的库没有. 但是随着越来越多的库不断的发展, 也在填补这个空白:

  1. jQuery不仅提供api用于和DOM元素交互, 也有许多api处理 object, array
  2. UnderScore也提供了很多有用的函数.
  3. Lodash也做了很多努力.
  4. Ramda也将很多函数式编程的思想带入js世界.

这些库的功能, 比如:map, flatMap, filter, and reduce也成为js内置库的一部分.这些库, 函数对ts有非常好的支持, 我们应该尽量使用这些库.

比如你想解析 CSV 数据, 你用自己命令式语句去写:

onst csvData = "...";
const rawRows = csvData.split('\n');
const headers = rawRows[0].split(',');

const rows = rawRows.slice(1).map(rowStr => {
  const row = {};
  rowStr.split(',').forEach((val, j) => {
    row[headers[j]] = val;
  });
  return row;
});

其中可以用reduce去简化:

const rows = rawRows.slice(1)
    .map(rowStr => rowStr.split(',').reduce(
        (row, val, i) => (row[headers[i]] = val, row),
        {}));

但是更好的办法是使用 Lodash 的zipObject函数:

import _ from 'lodash';
const rows = rawRows.slice(1)
    .map(rowStr => _.zipObject(headers, rowStr.split(',')));

这让代码显得简洁明了. 但是这值得引入一个新的库吗? 当然因为在ts中, 第一个方法会报错:

const rowsA = rawRows.slice(1).map(rowStr => {
  const row = {};
  rowStr.split(',').forEach((val, j) => {
    row[headers[j]] = val;
 // ~~~~~~~~~~~~~~~ No index signature with a parameter of
 //                 type 'string' was found on type '{}'
  });
  return row;
});
const rowsB = rawRows.slice(1)
  .map(rowStr => rowStr.split(',').reduce(
      (row, val, i) => (row[headers[i]] = val, row),
                     // ~~~~~~~~~~~~~~~ No index signature with a parameter of
                     //                 type 'string' was found on type '{}'
      {}));

解决办法是,给每个{}添加显示类型申明: {[column: string]: string} or Record<string, string>

Lodash的版本, 没有报错:

const rows = rawRows.slice(1)
    .map(rowStr => _.zipObject(headers, rowStr.split(',')));
    // Type is _.Dictionary<string>[]

Dictionary是Lodash的类型别名, 相当于:{[column: string]: string} or Record<string, string>. 重要的是这里不需要类型声明.

随着处理数据越来越复杂, 这样的优势越来越明显,比如你有一批nba球队的名单:

interface BasketballPlayer {
  name: string;
  team: string;
  salary: number;
}
declare const rosters: {[team: string]: BasketballPlayer[]};

如果你想获得所有球员的数组, 用js内置的concat处理这批数据, 就无法通过类型检查:

let allPlayers = [];
 // ~~~~~~~~~~ Variable 'allPlayers' implicitly has type 'any[]'
 //            in some locations where its type cannot be determined
for (const players of Object.values(rosters)) {
  allPlayers = allPlayers.concat(players);
            // ~~~~~~~~~~ Variable 'allPlayers' implicitly has an 'any[]' type
}

解决办法也是显性类型申明:

let allPlayers: BasketballPlayer[] = [];
for (const players of Object.values(rosters)) {
  allPlayers = allPlayers.concat(players);  // OK
}

更好的办法是用flat函数.:

const allPlayers = Object.values(rosters).flat();
// OK, type is BasketballPlayer[]

flat 能展开多维数组, 类型签名: T[][] => T[].这个版本更精确, 也不需要类型申明.

假如你想获得每个队薪水最高的球员们组成的数组. 最原始的版本:

const teamToPlayers: {[team: string]: BasketballPlayer[]} = {};
for (const player of allPlayers) {
  const {team} = player;
  teamToPlayers[team] = teamToPlayers[team] || [];
  teamToPlayers[team].push(player);
}

for (const players of Object.values(teamToPlayers)) {
  players.sort((a, b) => b.salary - a.salary);
}

const bestPaid = Object.values(teamToPlayers).map(players => players[0]);
bestPaid.sort((playerA, playerB) => playerB.salary - playerA.salary);
console.log(bestPaid);

这是输出:

  { team: 'GSW', salary: 37457154, name: 'Stephen Curry' },
  { team: 'HOU', salary: 35654150, name: 'Chris Paul' },
  { team: 'LAL', salary: 35654150, name: 'LeBron James' },
  { team: 'OKC', salary: 35654150, name: 'Russell Westbrook' },
  { team: 'DET', salary: 32088932, name: 'Blake Griffin' },
  ...
]

另外还有lodash的版本:

const bestPaid = _(allPlayers)
  .groupBy(player => player.team)
  .mapValues(players => _.maxBy(players, p => p.salary)!)
  .values()
  .sortBy(p => -p.salary)
  .value()  // Type is BasketballPlayer[]

这个版本更精确而且简洁, 不需要类型声明. 同时他将这样的操作:

_.a(_.b(_.c(v)))

变为了链式操作:

_(v).a().b().c().value()

Lodash中的奇怪的简写也被ts非常好的兼容了. 例如, Lodash 的_map 会比js的map更好用:

onst namesA = allPlayers.map(player => player.name)  // Type is string[]
const namesB = _.map(allPlayers, player => player.name)  // Type is string[]
const namesC = _.map(allPlayers, 'name');  // Type is string[]

ts的类型推断比c++, java的类型推断更神奇:

const salaries = _.map(allPlayers, 'salary');  // Type is number[]
const teams = _.map(allPlayers, 'team');  // Type is string[]
const mix = _.map(allPlayers, Math.random() < 0.5 ? 'name' : 'salary');
  // Type is (string | number)[]

所以我们应该使用函数构造和库来帮助类型流动