精进TypeScript--选择更精确的字符串类型的替代类型

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代码带来类型安全的有力的方法。使用更精确的类型既有助于捕捉错误,又有提高代码的可读性。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

~卷心菜~

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值