一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第25天,点击查看活动详情。
本文的翻译于<<Effective TypeScript>>, 特别感谢!! ps: 本文会用简洁, 易懂的语言描述原书的所有要点. 如果能看懂这文章,将节省许多阅读时间. 如果看不懂,务必给我留言, 我回去修改.
技巧33:用更精准的类型替代 string
string 的范围太广了。当你使用 string 作为类型的时候,就要考虑:能否更一步精确类型的范围?
假定你正在制作一个音乐收藏夹,你为专辑设计了一个type:
interface Album {
artist: string;
title: string;
releaseDate: string; // YYYY-MM-DD
recordingType: string; // E.g., "live" or "studio"
}
上面的注释提示了字符串的格式,但是由于没有约束力,依旧可能会出错:
const kindOfBlue: Album = {
artist: 'Miles Davis',
title: 'Kind of Blue',
releaseDate: 'August 17th, 1959', // Oops!
recordingType: 'Studio', // Oops!
}; // OK
其中的releaseDate,recordingType 字段,和预想的不太一样。但是依旧能通过 ts 的检查。但是会在使用中报错:
function recordRelease(title: string, date: string) { /* ... */ }
recordRelease(kindOfBlue.releaseDate, kindOfBlue.title); // OK, should be error
这其实是有更好的实现, releaseDate 用更精准的 Data 类型, recordingType 用更精准的:'studio' | 'live';:
type RecordingType = 'studio' | 'live';
interface Album {
artist: string;
title: string;
releaseDate: Date;
recordingType: RecordingType;
}
这样如果在申明Album类型的变量,出现拼写错误就会报错:
const kindOfBlue: Album = {
artist: 'Miles Davis',
title: 'Kind of Blue',
releaseDate: new Date('1959-08-17'),
recordingType: 'Studio'
// ~~~~~~~~~~~~ Type '"Studio"' is not assignable to type 'RecordingType'
};
同时用更精准的类型,不仅能提前发现错误,还能增加代码可读性。例如你想写一个函数:通过recordingType找到对应个专辑:
function getAlbumsOfType(recordingType: string): Album[] {
// ...
}
如果用上面的写法,该函数的调用者都不太清楚,recordingType具体指的是什么?如果这样定义:
/** What type of environment was this recording made in? */
type RecordingType = 'live' | 'studio';
function getAlbumsOfType(recordingType: RecordingType): Album[] {
// ...
}
当你把鼠标悬浮到recordingType上,ide就会告诉你类型:
还有一个常见的string的错误使用:函数的参数。例如,假设您想编写一个函数,用于提取数组中单个字段的所有值:
function pluck(records, key) {
return record.map(record => record[key]);
}
如何给上面代码添加类型?你可能会这样做:
function pluck(record: any[], key: string): any[] {
return record.map(r => r[key]);
}
但是这样使用any,会导致很多问题。我们选择用泛型来精确类型:
function pluck<T>(record: T[], key: string): any[] {
return record.map(r => r[key]);
// ~~~~~~ Element implicitly has an 'any' type
// because type '{}' has no index signature
}
报错由于 key 是 string 类型,太宽泛了。我们要求 key 都是T的字段,也就是:
type K = keyof Album;
// Type is "artist" | "title" | "releaseDate" | "recordingType"
function pluck<T>(record: T[], key: keyof T) {
return record.map(r => r[key]);
}
上面的改进,不仅能通过ts的检查,同时还能让ts精确推导出pluck函数返回值类型:
function pluck<T>(record: T[], key: keyof T): T[keyof T][]
例如:
const releaseDates = pluck(albums, 'releaseDate'); // Type is (string | Date)[]
返回值类型应该是Date[],而不是(string | Date)[],说明还不够精确。我们可以进一步改进:
function pluck<T, K extends keyof T>(record: T[], key: K): T[K][] {
return record.map(r => r[key]);
}
这样我们佳能正确使用了:
pluck(albums, 'releaseDate'); // Type is Date[]
pluck(albums, 'artist'); // Type is string[]
pluck(albums, 'recordingType'); // Type is RecordingType[]
pluck(albums, 'recordingDate');
// ~~~~~~~~~~~~~~~ Argument of type '"recordingDate"' is not
// assignable to parameter of type ...
同时也能使用自动补全的功能了:
当我们使用不合理的 string,就像 any一样会造成许多问题。因为其让非法的值通过检查,隐藏了 types 的之间的关系,扰乱了ts的类型检查,隐藏了真正的bug。所以我们定义类型的时候,尽可能的精确。