sting类型的域很大,当你声明一个类型为string的变量时,你应该问问自己更窄的类型是否合适。
- 避免使用“字符串类型”的代码。选择更合适的类型,不是每个地方都有可能是string
- 相比string,更倾向字符串字面量类型的联合,这会得到更严格的类型检查,和更好的开发体验
- 对于应该是对象属性的函数参数来说,相比string更倾向于使用keyof T
假设需要建立一个音乐集,想为一个专辑定义一个类型。看个示例:
interface Album {
artist: string;
title: string;
releaseDate: string; // YYYY-MM-DD
recordingType: string; // eg: 'live' or 'studio'
}
string类型的普遍性和注释中的类型信息强烈地表明这个interface是不太对的。下面是可能出错的地方:
const kindOfBlue: Album = {
artist: 'Davis',
title: 'kind of blue',
releaseDate: 'August 17th, 1959', // 格式和注释不符
recordingType: 'Studio', // 大写开头Studio
} // OK
releaseDate这个字段的格式不正确,"Studio"在应该小写的地方大写了。但是这些值都是字符串,所以这个对象是可以赋值为Album的,类型检查器也不会报错。
即使对于合法的Album对象,这些宽泛的string类型也可以掩盖错误,比如:
function recordRelease(title: string, date: string) { /*...*/ }
recordRelease(kindOfBlue.releaseDate, kindOfBlue.title); // OK,但应该提示错误
在调用recordRelease中,参数被颠倒了,但两个参数都是字符串,所以类型检查器不会报错。由于string类型的普遍性,这样的代码有时被称为“字符串类型化”。
我们可以把类型定义得更窄一些,以防这类问题的发生。比如releaseDate字段,最好用一个Date对象来避免格式相关的问题;recordingType定义一个只有两个值的联合类型(也可以使用enum,但一般建议避免使用枚举)
type RecordingType = 'studio' | 'live';
interface Album {
artlist: string;
title: string;
releaseDate: Date;
recordingType: RecordingType;
}
// 这样,TypeScript就可以进行更彻底的错误检查
const kindOfBlue: Album = {
artist: 'Davis',
title: 'kind of blue',
releaseDate: new Date('1959-08-17'),
recordingType: 'Studio', // ~~不能将类型Studio分配给‘RecordingType’
};
除了更严格的检查之外,这还有一些优点。
1.明确定义类型可以确保它的意义不会在传递过程中丢失。例如,如果你想查找某个录音类型的专辑,有这样一个函数:
function getAlbumsOfType(recordingType: string): Album[] { /*...*} }
该函数的调用者怎么知道被期待的recordingType是什么?它只是一个string,而解释它为“studio”或“live”的注释隐藏在Album的定义中,用户可能不会想到去看。
2.显示定义一个类型允许你给它附上文档:
/** 这段录音是在什么样的环境下录制的? */
type RecordingType = 'studio' | 'live';
当你把getAlbumsOfType的入参改为RecordingType的话,调用者就能通过点击来查看注释文档了。
还有一个常见的string误用是在函数参数中。假设有一个函数,将一个数组中某一个字段的所以值都提取出来。
function pluck(records, key) {
return records.map(record => record[key])
}
// 尝试给参数添加类型
function pluck(records: any[], key: string): any[] {
return records.map(record => record[key])
}
上面的类型虽说可以通过类型检查,但不是很好。首先,any类型是有问题的,特别是在返回值上。改进类型签名的第一步是引入一个泛型类型参数:
function pluck(records: T[], key: string): any[] {
return records.map(record => record[key]); // ~~对象“{}”类型的索引签名隐式地含有“any”类型
}
报错说用以key的string类型太宽泛,这个的确应该报错;如果你传入一个Album数组,那么key只有四个有效值(“artist” “title” “releaseDate” “recordingType”);而不是一个巨大的字符串集。这正是keyof Album类型的含义:
type K = keyof Album; // “artist”|“title”|“releaseDate”|“recordingType”
// 修复的方法用keyof T代替string:
function pluck<T>(records: T[], key: keyof T) {
return records.map(record => record[key]);
}
如此一来就能通过类型检查器。而且TypeScript推断了返回类型。如果你在编辑器中将鼠标移到pluck上,则推断类型是:
function pluck<T>(records: T[], key: keyof T): T[keyof T][]
T[keyof T]是T的任何可能的值的类型。如果你传入一个字符串作为key,这就太宽泛了。比如:
const releaseDates = pluck(albums, 'releaseDate'); // 类型是 (string|Date)[]
类型应该是Date[],而不是(string|Date)[]。虽然keyof T比string窄了很多,但还是太宽泛了。为了进一步缩小范围,我们需要引入第二个泛型参数,它是keyof T的一个子集(可能是一个单一的值):
function pluck<T, K extends keyof T>(records: T[], key: keyof T): T[K][] {
return records.map(record => record[key]);
}
现在类型签名是完全正确的。检验一下:
pluck(albums, 'releaseDate'); // 类型是 Date[]
pluck(albums, 'artist'); // 类型是 string[]
pluck(albums, 'recordingType'); // 类型是 RecordingType[]
pluck(albums, 'recordingDate'); // ~~类型“recordingDate”的参数不能赋给类型“...”的参数
string有一些跟any同样的问题:如果使用不当的话,它会放过不合法的值并隐藏掉类型之间的关系。这会阻碍类型检查器并可能隐藏真正的错误。TypeScript定义string子集的能力是为JavaScript代码带来类型安全的有力的方法。使用更精确的类型既有助于捕捉错误,又有提高代码的可读性。