写给TS泛型多次从入门到放弃的人的泛型入门教程

写给TS泛型多次从入门到放弃的人的泛型入门教程1. TypeScript的泛型太难了? 你是否对上述这样的泛型代码感到困惑? 如果你 (1)刚接触TypeScript, (2)刚接触泛型, (3)正在努力理解泛型

TypeScript Generics for People Who Gave Up on Understanding Generics

原文链接: ts.chibicode.com/generics/

作者的话: 我的教程是为了帮助初级程序员学习TypeScript,可能对有经验的程序员不那么有用。

为什么要针对初级程序员?随着 TypeScript 的普及,越来越多的初级程序员将学习和使用它。然而,我注意到许多现有的教程对初级程序员并不那么友好,因此,我尝试着写一个更友好的学习教程。

1. TypeScript的泛型太难了?

// Confused by generics code like this?
function makePair<
    F extends number | string,
    S extends boolean | F
>()

你是否对上述这样的泛型代码感到困惑?

如果你 (1)刚接触TypeScript, (2)刚接触泛型, (3)正在努力理解泛型,那么你就和13年前刚刚学习Java时的我一模一样。

与TypeScript一样,Java编程语言也支持泛型。当我在大学学习Java时,我是一个初级程序员,泛型对我来说感觉非常困难。所以我当时放弃了对泛型的理解,在不知道自己在做什么的情况下就使用了它们。直到大学毕业后找到一份全职工作,我才理解了泛型。

也许你和13年前的我一样,觉得TypeScript泛型太难了。如果是这样,这个教程就是为你准备的! 我将尝试帮助你去真正地理解泛型。

2. makeState()

首先,我在下面创建了一个名为makeState()的函数。我们将使用这个函数来讨论泛型。

function makeState() {
    let state: number
    function getState() {
        return state
    }
    function setState(x: number) {
        state = x
    }
    return { getState, setState }
}

当你运行 makeState() 时,它返回两个函数:getState()setState()。你可以使用这些函数来设置和获取名为state的变量。

让我们来试试吧! 当你运行下面的代码时,什么会被打印到控制台?先试着猜一下,然后按下运行按钮. (注:请进入原文进行操作)

image.png

它打印了1,然后是2。很简单,对吗?

附注

如果你使用过React,你可能已经意识到makeState()useState()钩子函数相似。

你感到困惑吗?有些人可能会疑惑。”为什么我们在另一个函数里面有函数?”或者”{ getState, setState }的语法是什么?”如果是这样,请点击这里获得更多解释。

  • 上面的代码在函数makeState()里面有函数getState(), setState()。这是因为我使用了闭包,它是JavaScript中最重要的概念之一。如果你没有听说过闭包,我强烈建议你去谷歌一下。这里有一篇MDN的文章
  • { getState, setState } 语法是JavaScript的ES2015特性(速记属性名称和对象重构 shorthand property names 和 object destructuring)。如果你没有见过,我建议你学习ES2015的语法–这里有一个很好的资料

3. 如果我们使用一个字符串呢?

现在,如果我们不使用1或2这样的数字,而是使用'foo'这样的字符串,会发生什么?先试着猜一下,然后按下编译按钮。 (注:请进入原文进行操作)

image.png

编译失败了,因为setState()需要传入一个数字。

image.png

为了解决这个问题,我们可以将statex的类型从数字改为字符串。

image.png

现在它可以工作了! 按下运行试试吧。

image.png

4. 挑战:两个不同的state状态

现在,我们已经掌握了基本知识,这里有一个给你的挑战。

问题:我们能否修改makeState(),使其能够创建两种不同的状态:一种只允许接收数字,另一种只允许接收字符串?

这就是我的意思: image.png

在前面,我们的第一个makeState()创建了纯数字的状态,第二个makeState()创建了纯字符串的状态。然而,它不能同时创建纯数字的状态和纯字符串的状态。

我们如何修改makeState()来实现我们的目标?

5. 尝试1

这里是第一种尝试。它能正常工作吗?

function makeState() {
    let state: number | string
    function getState() {
        return state
    }
    function setState(x: number | string) {
        state = x
    }
    return { getState, setState }
}

这样做是行不通的。 你最终会创建一个同时允许数字和字符串的状态,这不是我们想要的。相反,我们希望makeState()支持创建两种不同的状态:一种是只允许数字,另一种是只允许字符串

6. 尝试2: 泛型

这就是泛型的作用,请看下面的例子。

function makeState<S>() {
  let state: S
  function getState() {
    return state
  }
  function setState(x: S) {
    state = x
  }
  return { getState, setState }
}

makeState() 现在被定义为 makeState<S>()。你可以把<S>看作是你在调用该函数时必须传入的另一个东西。但你不是传递一个值,而是传递一个类型给它。

例如,当你调用makeState()时,你可以把类型数字作为S传递。

// It sets S as number
makeState<number>()

然后,在makeState()的函数定义里面,S将变成数字类型。

// In the function definition of makeState()
let state: S // <- number
function setState(x: S /* <- number */) {
    state = x
}

因为state将是数字类型,而setState只接受数字,所以它创建了一个纯数字的状态。

image.png

另一方面,为了创建一个纯字符串的state,你可以在调用makeState()时将字符串作为S传递。

注意:我们称 makeState<S>() 为 “泛型函数“,因为它实际上是泛型的–你可以选择让它只包含数字或只包含字符串。如果它接受一个类型参数,你就知道它是一个通用函数。

附注

你可能想知道。为什么我们将类型参数命名为 “S”?

答案是。它实际上可以是任何名字,但通常人们使用一个词的第一个字母来描述该类型所代表的内容。在这种情况下,我选择了 “S“,因为它描述的是 “S “状态的类型。以下名称也很常见。

  • T(代表 “T “类型)
  • E (代表 “E “lement)
  • K (代表 “K “ey)
  • V (代表 “V “alue)

7. 问题:你可以创建一个布尔类型的state

但是等一下,如果你把boolean传给S,你就可以创建一个只有boolean的状态。

// Creates a boolean-only state
const boolState = makeState<boolean>()
boolState.setState(true)
console.log(boolState.getState())

假设我们不希望makeState()能够创建非数字或非字符串的state(如布尔值)。我们怎样才能确保这一点呢?

解决办法。当你声明makeState()时,你把类型参数<S>改为<S extends number|string>。这是你唯一需要做的改变。

image.png

通过这样做,当你调用makeState()时,你只能将数字、字符串或任何其他扩展了数字或字符串的类型作为S传递。

让我们看看当你试图将布尔值作为S传递时会发生什么。 (注:请进入原文进行操作)

image.png

它产生了一个错误,而这正是我们想要的。我们已经成功地阻止了makeState()创建非数字或非字符串状态。

正如你刚才看到的,你可以指定泛型函数的类型参数是你允许的范围。

8. 默认类型

每次调用makeState()时都要指定<string><number>这样的类型,这很烦人。

所以我有个想法。我们能不能让<number>成为makeState()的默认类型参数?我们想让它变成这样,如果S没有被指定,它将被默认设置为number

为了实现这一点,我们可以通过在最后添加=number来指定S的默认类型。这有点像为普通函数参数设置默认值,不是吗?

image.png

通过这样做,你可以在不指定类型的情况下创建一个纯数字的状态。

image.png

9. 快速回顾:就像普通函数参数一样

我们已经完成了本文约三分之二的内容。在我们继续之前,让我们做一个简单的回顾。

你应该记住的是,泛型就像普通的函数参数。不同的是,普通函数参数处理的是,而泛型处理的是类型参数

例1: 例如,这里有一个普通的函数,可以传入任何类型的值。

// Declare a regular function
function regularFunc(x: any) {
    // You can use x here
}
// Call it: x will be 1
regularFunc(1)

类似地,你可以声明一个带有类型参数的泛型函数。

// Declare a generic function
function genericFunc<T>() {
// You can use T here
}
// Call it: T will be number
genericFunc<number>()

例2:在常规函数中,你可以这样指定一个参数的类型。

// Specify x to be number
function regularFunc(x: number)
// Success
regularFunc(1)
// Error
regularFunc('foo')

同样地,你可以指定泛型函数的类型参数允许的内容。

// Limits the type of T
function genericFunc<T extends number>()
// Success
genericFunc<number>()
// Error
genericFunc<string>()

例3:在常规函数中,你可以这样指定一个参数的默认值。

// Set the default value of x
function regularFunc(x = 2)
// x will be 2 inside the function
regularFunc()

同样地,你可以为一个泛型函数指定默认类型。

// Set the default type of T
function genericFunc<T = number>()
// T will be number inside the function
genericFunc()

泛型并不可怕。它们就像普通的函数参数,但它处理的是类型,而不是值。如果你理解了这么多,你就可以继续看下去了!

10. 让我们谈谈 makePair

让我们来看看新的函数makePair(),它类似于makeState(),但它不是存储一个单一的值,而是存储一对值,如{ first: ? , second: ? }, 现在,它只支持数字。

function makePair() {
// Stores a pair of values
let pair: { first: number; second: number }
function getPair() {
    return pair
}

// Stores x as first and y as second
function setPair(x: number, y: number) {
    pair = {
        first: x,
        second: y
    }
}
return { getPair, setPair }
}

让我们试试吧! 当你运行下面的代码时,控制台会打印出什么?先试着猜一下,然后按下运行按钮。 (注:请进入原文进行操作)

image.png

现在,就像我们对makeState()所做的那样,让我们把makePair()变成一个通用函数。

11. 泛型makePair

这里有一个泛型版本的makePair。

  • 它需要两个类型参数FS(代表 “F”irst 和 “S”econd)。
  • 第一个的类型将是F
  • 第二个的类型将是S
function makePair<F, S>() {
let pair: { first: F; second: S }

function getPair() {
    return pair
}

function setPair(x: F, y: S) {
    pair = {
        first: x,
        second: y
    }
}
return { getPair, setPair }
}

这里有一个用法的例子。调用makePair并且传入<number, string>,它要求第一个参数类型是数字,第二个参数类型是字符串。

// Creates a (number, string) pair
const { getPair, setPair } = makePair<
    number,
    string
>()
// Must pass (number, string)
setPair(1, 'hello')

总而言之,你可以创建一个接受多个类型参数的通用函数。

// makeState() has 1 type parameter
function makeState<S>()

// makePair() has 2 type parameters
function makePair<F, S>()

当然,你也可以像以前一样使用extends关键字或默认类型


function makePair<
  F extends number | string = number,
  S extends number | string = number
>()

你甚至可以让第二种类型(S)与第一种类型(F)相关。这里有一个例子。

image.png

11. 泛型接口(Generic interfaces)和类型别名(type aliases)

让我们回到我们之前的makePair()的实现。现在,看一下正确的类型。

image.png

上面的代码可以按原样工作,但如果我们想的话,我们可以把{ first: F, second: S }变成一个接口类型别名,这样它就可以被重用了。

让我们首先把配对的类型提取到一个泛型接口中。我将使用AB作为类型参数名,以区别于makePair()的类型参数。

// Extract into a generic interface
// to make it reusable
interface Pair<A, B> {
    first: A
    second: B
}

然后我们可以使用这个接口来声明类型。

function makePair<F, S>() {
    // Usage: Pass F for A and S for B
    let pair: Pair<F, S>
    // ...
}

通过提取到一个泛型接口(一个接受类型参数的接口),我们可以在必要时在其他地方重新使用它。

或者,我们也可以把它提取到一个通用类型别名中。对于对象类型,类型别名基本上与接口相同,所以你可以使用你喜欢的任何一种。

// Extract into a generic type alias. It’s
// basically identical to using an interface
type Pair<A, B> = {
    first: A
    second: B
}

总而言之,你可以创建通用接口和类型别名,就像你可以创建泛型函数一样。

附注 要了解更多关于接口和类型别名的信息,请阅读StackOverflow的答案StackOverflow answer。从TypeScript 3.7开始,它增加了对递归类型别名的支持,类型别名可以涵盖接口的几乎所有使用情况。support for recursive type aliases

13. 泛型类

我们要讲的最后一个是泛型类。首先,让我们重温一下makeState()的代码。这是不使用扩展或泛型类型参数的通用版本。

function makeState<S>() {
  let state: S
  function getState() {
    return state
  }
  function setState(x: S) {
    state = x
  }
  return { getState, setState }
}

我们可以把makeState()变成一个叫做State的泛型类,如下列代码。它看起来与makeState()相似,对吗?

class State<S> {
    state: S
    getState() {
        return this.state
    }
    setState(x: S) {
        this.state = x
    }
}

要使用泛型类,只需要在初始化的时候传入一个类型参数。

// Pass a type parameter on initialization
const numState = new State<number>()
numState.setState(1)
// Prints 1
console.log(numState.getState())

总而言之,泛型类就像泛型函数一样。泛型函数在我们调用它时需要一个类型参数,但泛型类在我们实例化它时需要一个类型参数。

附注 你需要在TypeScript配置(tsconfig.json)上设置 "strictPropertyInitialization":false,以使上述代码能够编译。

今天的文章写给TS泛型多次从入门到放弃的人的泛型入门教程分享到此就结束了,感谢您的阅读。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/23111.html

(0)
编程小号编程小号

相关推荐

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注