一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第19天,点击查看活动详情。
本文的翻译于<<Effective TypeScript>>, 特别感谢!! ps: 本文会用简洁, 易懂的语言描述原书的所有要点. 如果能看懂这文章,将节省许多阅读时间. 如果看不懂,务必给我留言, 我回去修改.
技巧27: 使用函数构造和库来帮助类型流动
在python, C, java中有很多好用的内置库, 但是js的内置的库没有. 但是随着越来越多的库不断的发展, 也在填补这个空白:
- jQuery不仅提供api用于和DOM元素交互, 也有许多api处理 object, array
- UnderScore也提供了很多有用的函数.
- Lodash也做了很多努力.
- 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)[]
所以我们应该使用函数构造和库来帮助类型流动