Typescript中函数声明中的尖括号“<>”代表什么?

13
在TypeScript代码中,我经常看到用尖括号包裹的代码,就像HTML一样。我知道它们不是HTML元素,也知道尖括号内的代码是类型,但是我经常看到没有尖括号的类型写法。似乎将类型包裹在尖括号中有非常具体和基础的目的,我觉得很多我不理解的内容可以从这个问题的答案中推断出来。
我想知道为什么尖括号是TypeScript语言的一部分,尖括号在程序上的作用是什么,尖括号对其中的代码有什么影响。
例如:这里添加尖括号的目的是什么?我应该如何解释它们?
getContent<K extends keyof ContentMap>(content: K, conf?: ContentMap[K]["conf"]): Promise<Readonly<ContentMap[K]["content"]>>;



3
另一个问题与此不相似。它询问了角括号的另一种用法(类型断言)。而这个问题则是关于泛型的。文档中已有解释。 - axiac
3个回答

44
当你学习Typescript时,你实际上要学两个语言。第一种语言是Typescript本身,它是带有类型注解和一些扩展的Javascript,例如“枚举”或“公共/私有”类成员。第二种语言是类型语言。它没有官方名称,我们可以称之为Anders,以发明者安德斯·海尔斯伯格的名字来命名。
Anders的目的是为您的程序生成动态类型。虽然Typescript操作字符串、数字、对象等值,但Anders只处理一种类型的数据:类型本身。Anders的值是类型。Anders中的函数接受一个或多个类型参数并返回另一个类型。
每次在程序中使用<>时,实际上你写的是Anders代码,而不是Typescript代码。这段代码可以被显式地调用(当你写像MyType<T>这样的东西时),也可以通过类型推断在幕后隐蔽地调用。
例如,下面是一个Typescript函数,它接受两个值并基于它们返回另一个值:
function pair (x, y) {
    return [x, y]
}

这是Anders函数,它接受两种类型并根据它们返回另一种类型:

type Pair<U, V> = [U, V]

在Typescript中,如果您给pair两个值,您将得到这两个值的数组。
在Anders中,如果您给Pair一个number(不是任意数字,而是"number"类型)和一个string,您将得到[number, string],这是所有可能的number,string数组的类型,例如[1, "hi"][3.14, "hey"]。如果您给它stringboolean,您将得到所有类似["hi", true]["blah", false]的数组类型。
像其他语言一样,Anders提供基本的编程结构(总之,所有这些都是类型或作用于类型,而不是值):
  • built-in types, like number, string, any, {}. These are similar to Typescript built-in objects like "Number" or "String".

  • literals, like "foo". These are similar to literals in Typescript, but while in TS "foo" means a specific string, e.g. a sequence of characters f, o, o, in Anders it means a type, namely, "the type of all strings that are foo", which, obviously, has only one possible member, "foo".

  • unions, similar to arrays in TS: A|B|C.

  • structures, similar to objects in TS. In TS, an object maps strings to values. In Anders, a structure (aka "mapped type"), maps types to other types. The index operator S[B] returns the type to which the structure S maps B

        {foo: string; bar:number}["foo"]` ====> string
    
  • operators, e.g. the unary keyof operator takes a type A and returns the type of all possible keys of A, that is, a union (array) TypeOfKey1 | TypeOfKey2 | ...

        keyof {foo:string, bar:number} =====> "foo"|"bar"
    
  • comparisons, like a > b in TS. Anders only has one form of comparison, A extends B, which means that A is a subset of B, that is, all possible values of the type A are also values of B, but not necessarily the other way around.

        "foo" extends string =====> ok
        "foo" extends "foo"|"bar" =====> ok
        "blag" extends "foo"|"bar" =====> not ok
    
  • conditionals: comparison ? Type1 : Type2

  • loops, like {[A in SomeUnion]: T}. This creates a structure, whose keys are the union members and values are of type T

        {[A in "foo"|"bar"]: number} =====> {foo:number, bar:number}
    
  • function calls, which are SomeOtherTypeDeclaration<Type1, Type2, ...>

  • finally, Anders also have type checks for input parameters, similar to function foo(x:number) in Typescript. In Anders, a type check is a comparison, that is, A extends B

现在,回到你的例子(为了清晰起见而简化)。

interface A {}
interface B {}
interface C {}
interface D {}

type ContentMap = {
  foo: {
      conf: A
      content: B
  },
  bar: {
      conf: C
      content: D
  }
}

function getContent<K extends keyof ContentMap>
  ( content: K,
    conf?: ContentMap[K]["conf"]
  ): Readonly<ContentMap[K]["content"]> {
      ...
  }

getContent是Anders函数,接受类型K并返回另一种类型(X,Y) => Z,它是所有具有两个类型为XY的参数和返回值为Z类型的函数的类型。

让我们手动使用不同的类型"调用"此函数并查看发生了什么。

  1. getContent<number>。首先,Anders检查参数的类型。我们的类型检查为extends keyof ContentMap。回想一下,keyof ContentMap返回ContentMap键的数组,即"foo"|"bar",其中"foo""bar"是类型而不仅仅是字符串。然后,我们的参数number"foo"|"bar"进行比较。显然,number不是这种类型的子集,因此类型检查失败,我们会得到一个错误。

  2. getContent<"foo">。类型检查成功(因为"foo""foo"|"bar"的子集),然后我们可以继续。我们的任务是基于"foo"构建函数类型。第一个参数的类型为K,与参数相同,所以它变成了"foo"。第二个参数应用索引运算符两次:首先,我们评估ContentMap["foo"],它给出{conf: A, content: B},然后我们应用["conf"],它给出A。以类似的方式,我们为返回类型获得B。最后,我们调用内置的Anders函数Readonly并得到另一种类型,让我们称之为ReadonlyB。因此,我们得到的是函数类型(content: "foo", conf: A) => ReadonlyB,这就是我们的Anders函数返回的内容。

  3. getContent<"bar">... 留作练习。

现在,当您编写这个时,会发生什么?

let something = getContent('foo', {...})

编译器会看到您有一些与 getContent 相关的 Anders 代码,并评估该代码,将 "foo" 作为参数传递。如上所示,返回类型将是 ("foo", A) => ReadonlyB。然后,上述行将与此类型进行检查,如果不匹配,则失败,这基本上就是整个过程的全部内容。
希望这可以帮助您...

哇,太神奇了!你在哪里学到这些东西的啊?我上过谷歌并查看了许多博客,但没有一个人解释得这么清楚。你应该写一本书,我肯定会买的! - Michael

19

正如@axiac所提到的那样,这与泛型有关。

你可以将其理解为类型

例如:

// generic class that deals with type T
class List<T> {}

// usage
const list1 = new List<string>() // list of type string
const list2 = new List<number>() // list of type number
const list3 = new List<any>()    // list of type any

3
比那个超长的被接受答案容易理解多了,谢谢! - Timo Ernst
1
@TimoErnst 是的,虽然简短的回答很好,但你应该更加欣赏长篇回答,因为其中的每个细节都对理解这个问题很重要。这个回答简洁明了,对于那些对这个话题一无所知的人来说是可以接受的,但最终每个精通TS的人都需要知道长篇回答提供的内容。我总是很感激长篇回答。 - JΛYDΞV

0

1
虽然这个链接可能回答了问题,但最好在此处包含答案的基本部分并提供参考链接。如果链接页面更改,仅有链接的答案可能会失效。-【来自审查】 - Quentin

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接