从字符串数组中提取自定义类型
在 TypeScript 的世界里,自定义类型从字符串数组中显现,就像隐藏的宝石。
TypeScript 是一个操纵现有数据和发展良好实践的神奇工具。
今天,我们将探索如何以正确的方式从字符串数组中提取全名,以确保产生干净的类型安全输出。
那么,不多说了……让我们直接开始吧。
问题
首先让我们通过检查这段代码来理解其中的问题:
const names = ["Daniel Craciun", "John Doe", "Harry Pigeon"]
const findName = (surname: string) => {
return names.find((name) => name.includes(surname))
}
// 我们可以传入任何字符串,这是不理想的。
console.log(findName("Craciun")) // 输出:Daniel Craciun
console.log(findName("Doee")) // 输出:undefined
这段代码使用一个名字数组来进行搜索。
函数 findName 接受一个字符串 surname 并返回关联的全名。
问题出现在当你在 findName 函数中输入 "Doee" 时。
这个不显眼的拼写错误导致输出了 undefined,这可能会导致后续的错误,因为没有任何东西阻止我们犯这种错误。
这就是 TypeScript 发挥作用的地方。
如果我们确保 findName 只接受字面上的姓氏,即 Craciun、Doe、Pigeon,那么当我们输入像 "Doee" 这样在名字数组中不存在的输入时,编译器应该会提出警告。
解决方案
我们已经确定了 findName 的有效参数只能是所有现有的姓氏。
为了实现这一点,我们创建了一个名为 ExtractSurname 的泛型类型。
ExtractSurname 的代码可能看起来有点复杂,但我们将一步步拆解它:
type ExtractSurname<T extends string> = T extends `${infer Firstname} ${infer Surname}` ? Surname : null
这里 ExtractSurname 接受一个泛型参数 T,它引用任何字面字符串,使用 extends 操作符。在 ExtractSurname<“Daniel”> 中,T 的值将等于 "Daniel"。
接下来我们应用 TypeScript 三元运算符,它类似于 JavaScript 三元运算符,但我们是在比较类型而不是实际数据。
我们知道我们的名字数组的格式是“<名> <姓>”,所以这里使用 infer 关键字从 T 中提取子类型。
在 ExtractSurname<“Daniel Craciun”> 中:
- infer Firstname = “Daniel”
- infer Surname = “Craciun”
最后,如果输入满足我们的“<名> <姓>”格式,返回 Surname 作为类型,否则返回 null。
好的,我们的 ExtractSurname 类型准备好了。
现在我们需要一个 Surname 类型来表示 names 中所有的姓氏。
type ExtractSurname<T extends string> = T extends `${infer Firstname} ${infer Surname}` ? Surname : null
const names = ["Daniel Craciun", "John Doe", "Harry Pigeon"] as const
type Surname = ExtractSurname<(typeof names)[number]>
names 满足 ExtractSurname 的格式 “*<名> <姓>*”。
我们使用 as const 将 names 的类型从字符串缩小到字面字符串数组。
这意味着我们转换 names 的类型从 string 到:readonly [“Daniel Craciun”, “John Doe”, “Harry Pigeon”]。
参数 (typeof names)[number] 代表 names 中每个索引元素的类型:“Daniel Craciun” | “John Doe” | “Harry Pigeon”
最终,这是 Surname 的结果类型:type Surname = “Craciun” | “Doe” | “Pigeon”
最后一步是用下面的新函数 findNameUsingSurname 更新我们之前定义的 findName 函数:
// 接收一个实际的 `Surname` 而不是一般的字符串。
const findNameUsingSurname = (surname: Surname) => {
// 注意:我们需要后缀运算符 "!" 来断言 "find" 函数不返回未定义的值。
return names.find((name) => name.includes(surname))!
}
// 唯一可接受的输入:"Craciun", "Doe", "Pigeon" = 最大类型安全
const fullName = findNameUsingSurname("Craciun")
// 输出:"Daniel Craciun"
console.log(fullName)
而这里是 TypeScript 编译器如我们所期待的那样施展它的魔法: