[译]<<Effective TypeScript>> 技巧33 用更精准的类型替代 string

152 阅读3分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 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就会告诉你类型:

image.png

还有一个常见的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 ...

同时也能使用自动补全的功能了:

image.png

当我们使用不合理的 string,就像 any一样会造成许多问题。因为其让非法的值通过检查,隐藏了 types 的之间的关系,扰乱了ts的类型检查,隐藏了真正的bug。所以我们定义类型的时候,尽可能的精确。