学习TypeScript的泛型

62 阅读4分钟

我想回顾一下这篇文字稿,并加以阐述。而这个系列被称为Tidy TypeScript,所以期待更多的意见性立场。

泛型编程#

TypeScript的泛型可以说是该语言中最强大的功能之一。它们为TypeScript自己的元编程语言打开了一扇门,它允许非常灵活和动态的类型生成。正如Anders Hejlsberg在其2020年TSConf主题演讲中所说,它真的很接近于成为自己的函数式编程语言。

特别是在最近的TypeScript版本中,随着字符串字面类型递归条件类型的到来,我们可以制作出能做出惊人之举的类型。这个小类型解析了Express风格的路由信息,并检索了一个带有所有参数的对象。

type ParseRouteParameters<T> = 
  T extends `${string}/:${infer U}/${infer R}` ? 
    { [P in U | keyof ParseRouteParameters<`/${R}`>]: string } : 
  T extends `${string}/:${infer U}` ?
    { [P in U]: string } : {}


type X = ParseRouteParameters<"/api/:what/:is/notyou/:happening">
// type X = {
//   what: string,
//   is: string,
//   happening: string, 
// }

强大的功能!(Dan在他的博客上展示了这个类型的一个更复杂的版本,请查看)。

当我们定义一个通用类型时,我们也定义了通用类型的参数。这就是角括号之间的东西,我们有时顺便称之为泛型

它们可以是某种类型(或者更正确的说法:是某种子类型)。

type Foo<T extends string> = ...

它们可以有默认值。

type Foo<T extends string = "hello"> = ...

当使用默认值时,顺序是很重要的。与普通的JavaScript函数有很多相似之处!那么,既然我们几乎是在谈论函数,为什么我们要为通用类型参数使用单字母名称呢?

命名通用类型参数#

大多数通用类型参数以字母T 开始。后续的参数沿着字母表走(U,V,W),或者是缩写,如Kkey

和几乎所有的编程概念一样,泛型的概念已经存在了相当长的时间。通用类型的一些主要实现可以在七十年代的编程语言中看到,比如AdaML

我不知道命名类型参数T ,是否从那时就开始了,还是因为C++中类似的--尽管更强大的--模板概念的流行,导致我们普遍这样称呼它们。关键是。我们这样做已经有很长一段时间了。我们已经习惯于此。

然而,这可能导致高度不可读的类型。如果我看一下Pick<T, U> ,我永远无法知道我是否从对象类型U 中提取了键T ,或者是否是对象类型T ,我在那里提取了键U

做得更详细一点会有很大的帮助。

type Pick<Obj, Keys> = ...

注意:实际的Pick 类型在TypeScript中的定义要好得多(用K extends keyof T ),但你会明白这个意思。Exclude,Extract,Record...所有这些都让我挠头。

因此,尽管为我们的泛型使用单字母名称是很常见的,但我认为我们可以做得更好!

一个命名的概念#

类型是文档,我们的类型参数可以有说话的名字。就像你对普通函数所做的那样。这是我正在使用的风格指南。

  1. 所有的类型参数都以大写字母开始。就像我给所有其他类型起名字一样!
  2. 只有在用途完全清楚的情况下才使用单字母。例如:ParseRouteParams 只能有一个参数,即路线。
  3. 不要缩写为T (那太......通用了!🤨),而要缩写为能让人清楚地知道我们在处理什么。例如:ParseRouteParams<R> ,其中R 代表Route
  4. 很少使用单字母,坚持使用短语,或缩写。Elem 代表ElementRoute 可以保持原样。
  5. 在我需要区别于内置类型的地方使用前缀。例如,Element ,我可以使用GElement (或坚持使用Elem )。
  6. 使用前缀使通用名称更清晰URLObjObj 更清晰,例如。
  7. 同样的模式也适用于通用类型中的推断类型。

让我们再看一下ParseRouteParams ,用我们的名字更明确。

type ParseRouteParameters<Route> = 
  Route extends `${string}/:${infer Param}/${infer Rest}` ? 
    { [Entry in Param | keyof ParseRouteParameters<`/${Rest}`>]: string } : 
  Route extends `${string}/:${infer Param}` ?
    { [Entry in Param]: string } : {}

每个类型的含义变得更清楚了。我们还看到,我们需要遍历Param 中的所有Entries ,即使Param 只是一个类型的集合。

可以说,比以前可读性强多了!