高级打字稿

自由潜水-没有潜水的水肺潜水。潜水员感受到了阿基米德的规律:他排出了一定量的水,这使他退缩了。因此,最开始的几米是最难承受的,但随后您上方水柱的压力开始帮助移至更深。这个过程让人想起学习和潜入TypeScript类型系统-当您潜入时会变得容易一些。但是我们不能忘记及时出现。一张海洋一口气的


照片

米哈伊尔·巴舒罗夫saitonakamura)-WiseBits的高级前端工程师,是TypeScript爱好者和业余爱好者。学习TypeScript和深入学习的类比并非偶然。Michael会告诉您什么是有区别的联合,如何使用类型推断,为什么需要名义上的兼容性和品牌。屏住呼吸潜水。

在此处演示和GitHub链接以及示例代码

什么样的工作


和的类型听起来是代数的-让我们尝试找出它是什么。它们在ReasonML中称为变体,在Haskell中称为带标记的并集,在F#和TypeScript中被区分为并集。

维基百科给出以下定义:“数量的类型是作品类型的总和。”
-谢谢队长!

该定义完全正确,但没有用,所以让我们理解。让我们从私人开始,从工作类型开始。

假设我们有一个type Task它有两个字段:id和创建它的人。

type Task = { id: number, whoCreated: number }

这种类型的工作是相交或相交这意味着我们可以分别编写与每个字段相同的代码。

type Task = { id: number } & { whoCreated: number }

在命题的代数中,这称为逻辑乘法:这是运算“与”或“与”。产品的类型是这两个元素的逻辑乘积。
具有一组字段的对象可以通过逻辑乘积表示。
工作类型不限于字段。想象我们爱原型,并决定我们不见了leftPad

type RichString = string & { leftPad: (toLength: number) => string }

将其添加到String.prototype为了表示类型,我们采用字符串和采用必要值的函数。方法和字符串是交集。

协会


联合— 联合 —例如,用于表示设置CSS中元素宽度的变量类型:一个10像素的字符串或一个绝对值。CSS规范实际上要复杂得多,但是为了简单起见,让我们保留它。

type Width = string | number

一个例子更复杂。假设我们有一个任务。它可以处于三种状态:刚刚创建,接受工作并完成。

type InitialTask = { id: number, whoCreated: number }
type InWorkTask = { id: number, whoCreated: number }
type FinishTask = { id: number, whoCreated: number, finishDate: Date }

type Task = InitialTask | InWorkTask | FinishedTask

在第一个和第二个状态中,任务具有id和创建者,在第三个状态中,显示完成日期。此处出现联合-这个或那个。

注意相反,type您可以使用interface但是有细微差别。联合和相交的结果始终是一种类型。您可以通过来表示交集extends,但不能通过interface表示交集type只是更短。

interface仅有助于合并声明。库类型始终需要通过来表示interface,因为这将允许其他开发人员使用其字段来扩展它。如此典型,例如jQuery插件。

类型兼容性


但是存在一个问题-在TypeScript中,结构类型兼容性。这意味着,如果第一类型和第二类型具有相同的字段集,则它们是相同类型的两个。例如,我们有10种类型,它们的名称不同,但结构相同(具有相同的字段)。如果我们向接受其中一种的函数提供10种类型中的任何一种,TypeScript不会介意。

相反,在Java或C#中,是名义上的兼容性系统。我们将使用相同的字段编写相同的10个类,并使用其中一个函数。如果其他类不是该函数打算使用的类型的直接后代,则该函数将不接受。

通过向字段添加状态来解决结构兼容性问题:enumstring。但是总和类型是一种比在数据库中存储方式更能表达语义的方式。

例如,我们将处理ReasonML。这就是这种类型的外观。

type Task =
| Initial({ id: int, whoCreated: int })
| InWork({ id: int, whoCreated: int })
| Finished({
        id: int,
        whoCreated: int,
        finshDate: Date
    })

同样的领域,除了int代替number。如果你仔细观察,我们注意到InitialInWorkFinished。它们不在类型名称的左侧,而是在定义的右侧。这不仅是标题中的一行,而且是单独类型的一部分,因此我们可以区分第一个和第二个。

生活骇客。对于通用类型(例如您的域实体),请创建文件global.d.ts并将所有类型添加到该文件中。它们将在整个TypeScript代码库中自动可见,而无需显式导入。这对于迁移很方便,因为您不必担心在哪里放置类型。

让我们使用原始Redux代码示例查看如何执行此操作。

export const taskReducer = (state, action) = > {
    switch (action.type) {
        case "TASK_FETCH":
            return { isFetching: true }
        case "TASK_SUCCESS":
            return { isFetching: false, task: action.payload }
        case "TASK_FAIL":
            return { isFetching: false, erro: action.payload }
    }
    
    return state
}

用TypeScript重写代码。让我们开始将文件-从重命名.js.ts。打开所有状态选项和noImplicitAny选项。此选项查找未指定参数类型的函数,并且在迁移阶段很有用。

可输入State:添加字段isFetchingTask(不能为)和Error

type Task = { title: string }

declare module "*.jpg" {
    const url: string
    export default url
}

declare module "*.png" {
    const url: string
    export default url
}

注意。在Basarat Ali Syed的TypeScript Deep Dive中偷窥了Lifehack 。它有些陈旧,但对于想深入学习TypeScript的人仍然有用。在Marius Schulz 博客上了解TypeScript的当前状态。他撰写了有关新功能和窍门的文章。还要研究变更日志,不仅有关于更新的信息,而且还有如何使用它们。

剩余的动作。我们将键入并声明每个。

type FetchAction = {
    type: "TASK_FETCH"
}

type SuccessAction = {
    type: "TASK_SUCCESS",
    payload: Task

type FailAction = {
    type: "TASK_FAIL",
    payload: Error
}

type Actions = FetchAction | SuccessAction | FailAction

type所有类型 的字段都不同,它不仅是字符串,而且是特定的字符串- 字符串常量这是非常有区别的 -我们用来区分所有情况的标签。我们得到了一个有区别的联合,例如,我们可以了解有效载荷中的内容。

更新原始的Redux代码:

export const taskReducer = (state: State, action: Actions): State = > {
    switch (action.type) {
        case "TASK_FETCH":
            return { isFetching: true }
        case "TASK_SUCCESS":
            return { isFetching: false, task: action.payload }
        case "TASK_FAIL":
            return { isFetching: false, error: action.payload }
    }
    
    return state
}

如果我们写string... 这里有危险
type FetchAction = {
    type: string
}
...那么一切都会破裂,因为它不再是歧视者。

在此操作中,type根本可以是任何字符串,而不是特定的区分符。之后,TypeScript将无法区分一个动作和另一个错误。因此,应该确切地是一个字符串文字。此外,我们可以添加以下设计:type ActionType = Actions["type"]

提示中将弹出我们的三个选项。



如果你写...
type FetchAction = {
    type: string
}
...然后提示将很简单string,因为所有其他行不再重要。



我们都输入了金额的类型。

详尽检查


想象一下一个没有将错误处理添加到代码中的假设情况。

export const taskReducer = (state: State, action: Actions): State = > {
    switch (action.type) {
        case "TASK_FETCH":
            return { isFetching: true }
        case "TASK_SUCCESS":
            return { isFetching: false, task: action.payload }
        //case "TASK_FAIL":
            //return { isFetching: false, error: action.payload }
    }
    
    return state
}

这里详尽的检查将对我们有帮助-另一个有用的属性,例如sum 这是验证我们已处理所有可能案件的机会。

在switch语句之后添加一个变量:const exhaustiveCheck: never = action

从来都不是一个有趣的类型:

  • 如果它是函数返回的结果,则该函数永远不会正确结束(例如,它总是抛出错误或无限执行);
  • 如果它是一种类型,那么无需任何额外的努力就无法创建此类型。

现在,编译器将指示错误“类型'FailAction'无法分配给类型'从不'”。我们有三种可能的操作类型,我们尚未处理"TASK_FAIL"-这是FailAction但是从来没有,它不是“可分配的”。

让我们添加processing "TASK_FAIL",将不再有错误。处理"TASK_FETCH"-返回,处理"TASK_SUCCESS"-返回,处理"TASK_FAIL"当我们处理所有三个功能时,可能会采取什么措施?没事- never

如果添加其他操作,编译器会告诉您哪些未处理。如果您想响应所有操作,并且仅响应选择性操作,则否定。

第一点

努力。
首先,将很难深入研究:了解总和的类型,然后了解链中的乘积,代数数据类型等。将不得不付出努力。

当我们深入潜水时,我们上方水柱的压力会增加。在一定深度处,我们上方的浮力和水压是平衡的。这是“中性浮力”区域,我们处于失重状态。在这种状态下,我们可以转向其他类型的系统或语言。

标称类型系统


在古罗马,居民的名字由三个部分组成:姓,名和“昵称”。名称本身就是“ nomen”。由此产生标称类型系统-“ nominal”。有时很有用。

例如,我们有这样的功能代码。

export const githubUrl = process.env.GITHUB_URL as string
export const nodeEnv = process.env.NODE_ENV as string

export const fetchStarredRepos = (
    nodeEnv: string,
    githubUrl: string
): Promise<GithubRepo[ ]> => {
    if (nodeEnv = "production") {
        // log call
    }

    // prettier-ignore
    return fetch('$githubUrl}/users/saitonakamura/starred')
        .then(r => r.json());
}

该配置来自API :GITHUB_URL=https://api.github.comgithubUrlAPI 方法提取出我们上面带有星号的存储库。如果为nodeEnv = "production",则它将记录此调用,例如,以获取指标。

对于我们要开发的功能(UI)。

import React, { useState } from "react"

export const StarredRepos = () => {
    const [starredRepos, setStarredRepos] = useState<GithubRepo[ ] | null>(null)

    if (!starredRepos) return <div>Loading…</div>

    return (
        <ul>
            {starredRepos.map(repo => (
                <li key={repo.name}>repo.name}</li>
            ))}
        </ul>
    )
}

type GithubRepo = {
    name: string
}

该功能已经知道如何显示数据,如果没有数据,则将显示一个加载器。仍然需要添加API调用并填充数据。

useEffect(() => {
    fetchStarredRepos(githubUrl, nodeEnv).then(data => setStarredRepos(data))
}, [ ])

但是,如果我们运行此代码,一切都会崩溃。在开发人员面板中,我们发现它正在fetch访问地址'/users/saitonakamura/starred'-githubUrl已消失在某个地方。事实证明,所有这些都是因为设计怪异-排nodeEnv在第一位。当然,更改所有内容可能很诱人,但是该功能可以在代码库的其他地方使用。

但是,如果编译器提前提示该怎么办?然后,您不必经历整个启动周期,检测到错误或搜索原因。

品牌推广


TypeScript为此提供了一个技巧-品牌类型。创建BrandB(字符串)T和两种类型。我们将为创建相同的类型NodeEnv

type Brand<T, B extends string> = T & { readonly _brand: B }

type GithubUrl = Brand<string, "githubUrl">
export const githubUrl = process.env.GITHUB_URL as GithubUrl

type NodeEnv = Brand<string, "nodeEnv">
export const nodeEnv = process.env.NODE_ENV as NodeEnv

但是我们得到一个错误。



现在githubUrlnodeEnv彼此分配不可能的,因为它们是名义上的类型。不显眼,但名义上。现在我们不能在这里交换它们-我们在代码的另一部分中更改它们。

useEffect(() => {
    fetchStarresRepos(nodeEnv, githubUrl).then(data => setStarredRepos(data))
}, [ ])

现在一切都好了,他们有了烙印的原语。当遇到多个参数(字符串,数字)时,商标很有用。它们具有一定的语义(x,y坐标),因此不应混淆。当编译器告诉您它们很困惑时,这很方便。

但是有两个问题。首先,TypeScript没有本机标称类型。但是,希望它能得到解决,语言存储库中正在进行有关此问题讨论

第二个问题是“ as”,不能保证GITHUB_URL链接不会中断。

以及NODE_ENV。最有可能的,我们希望不只是一些字符串,但"production"还是"development"

type NodeEnv = Brand<"production" | "development" | "nodeEnv">
export const nodeEnv = process.env.NODE_ENV as NodeEnv

所有这些都需要检查。我指的是聪明的设计师和Sergey Cherepanov的报告“ 使用功能样式的TypeScript设计域 ”。

第二和第三技巧

小心点。
有时停下来看看周围:其他语言,框架,类型系统。学习新原理并学习课程。

当我们经过“中性浮力”的点时,水就会更猛地向我们压下去。
放松。
深入研究,让TypeScript发挥作用。

TypeScript可以做什么


TypeScript可以打印类型

export const createFetch = () => ({
    type: "TASK_FETCH"
})

export const createSuccess = (task: Task) => ({
    type: "TASK_SUCCESS"
    payload: Task
})

export const createFail = (error: Error) => ({
    type: "TASK_FAIL"
    payload: error
})

type FetchAction = {
    type: "TASK_FETCH",

type SuccessAction = {
    type: "TASK_SUCCESS",
    payload: Task

type FailAction = {
    type: "TASK_FAIL",
    payload: Error
}

为此有TypeScript- ReturnType它从函数获取返回值:
type FetchAction = ReturnType<typeof createFetch>
在其中传递函数的类型。我们根本无法编写函数:要从函数或变量中获取类型,我们需要编写typeof

我们在提示中看到type: string



这很不好-区分符会中断,因为有一个对象文字。

export const createFetch = () => ({
    type: "TASK_FETCH"
})

当我们使用JavaScript创建对象时,默认情况下它是可变的。这意味着在具有字段和字符串的对象中,我们以后可以将字符串更改为另一个。因此,TypeScript将特定字符串扩展为可变对象的任何字符串。

我们需要以某种方式帮助TypeScript。为此有const。

export const createFetch = ( ) => ({
    type: "TASK_FETCH" as const
})

添加-字符串将立即在提示中消失。我们不仅可以跨行编写,而且通常可以写整个文字。

export const createFetch = ( ) => ({
    type: "TASK_FETCH"
} as const)

然后,类型(和所有字段)将变为只读。

type FetchAction = {
    readonly type: "TASK_FETCH";
}

这很有用,因为您不太可能更改操作的可变性。因此,我们到处都添加为const。

export const createFetch = () => ({
    type: "TASK_FETCH"
} as const)

export const createSuccess = (task: Task) => ({
    type: "TASK_SUCCESS"
    payload: Task
} as const)

export const createFail = (error: Error) => ({
    type: "TASK_FAIL"
    payload: error
} as const)

type Actions =
    | ReturnType<typeof createFetch>
    | ReturnType<typeof createSuccess>
    | ReturnType<typeof createFail>

type State =
    | { isFetching: true }
    | { isFetching: false; task: Task }
    | { isFetching: false; error: Error }

export const taskReducer = (state: State, action: Actions): State = > {
    switch (action.type) {
        case "TASK_FETCH":
            return { isFetching: true }
        case "TASK_SUCCESS":
            return { isFetching: false, task: action.payload }
        case "TASK_FAIL":
            return { isFetching: false, error: action.payload }
}

  const _exhaustiveCheck: never = action

  return state
}

减少了键入代码的所有操作,并将其添加为const。TypeScript理解了其他所有内容。

TypeScript可以输出State它由上面代码中的联合表示,具有isFetching的三种可能状态:true,false或Task。

使用类型State = ReturnType。TypeScript提示指示存在循环依赖关系。



缩短。

type State = ReturnType<typeof taskReducer>

export const taskReducer = (state, action: Actions) => {
    switch (action.type) {
        case "TASK_FETCH":
            return { isFetching: true }
        case "TASK_SUCCESS":
            return { isFetching: false, task: action.payload }
        case "TASK_FAIL":
            return { isFetching: false, error: action.payload }
}

const _exhaustiveCheck: never = action

return state
}

State停止诅咒,但现在他是any,因为我们有周期性的依赖关系。我们输入参数。

type State = ReturnType<typeof taskReducer>

export const taskReducer = (state: { isFetching: true }, action: Actions) => {
    switch (action.type) {
        case "TASK_FETCH":
            return { isFetching: true }
        case "TASK_SUCCESS":
            return { isFetching: false, task: action.payload }
        case "TASK_FAIL":
            return { isFetching: false, error: action.payload }
}

const _exhaustiveCheck: never = action

return state
}

结论已经准备好了。



结论是类似于我们原本有:truefalseTask这里有垃圾字段Error,但类型是undefined-字段似乎在那里,但似乎没有。

第四提示

不要劳累。
如果您放松并潜水得太深,则可能没有足够的氧气返回。

培训还包括:如果您过于沉迷于技术,并决定将其应用到任何地方,那么很可能会遇到错误,这是您不知道的原因。这将导致拒绝,并且将不再希望使用静态类型。尝试评估自己的力量。

TypeScript如何减慢开发速度


支持键入将需要一些时间。它没有最好的UX-有时它会给出完全无法理解的错误,就像其他类型的系统一样,例如Flow或Haskell。
系统表现力越高,错误越困难。
类型系统的价值在于它可以在发生错误时提供快速反馈。系统将显示错误,并花费更少的时间查找和纠正错误。如果您花费更少的时间纠正错误,那么更多的体系结构解决方案将受到更多关注。如果您学习使用类型,它们不会减慢开发速度。

++. - , , (25 26 ) - (27 — 10 ).

(5900 ), ++ IT-, .

AvitoTech FrontendConf 2019 . youtube- telegram @FrontendConfChannel, .

All Articles