自由潜水-没有潜水的水肺潜水。潜水员感受到了阿基米德的规律:他排出了一定量的水,这使他退缩了。因此,最开始的几米是最难承受的,但随后您上方水柱的压力开始帮助移至更深。这个过程让人想起学习和潜入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个类,并使用其中一个函数。如果其他类不是该函数打算使用的类型的直接后代,则该函数将不接受。通过向字段添加状态来解决结构兼容性问题:enum
或string
。但是总和类型是一种比在数据库中存储方式更能表达语义的方式。例如,我们将处理ReasonML。这就是这种类型的外观。type Task =
| Initial({ id: int, whoCreated: int })
| InWork({ id: int, whoCreated: int })
| Finished({
id: int,
whoCreated: int,
finshDate: Date
})
同样的领域,除了int
代替number
。如果你仔细观察,我们注意到Initial
,InWork
和Finished
。它们不在类型名称的左侧,而是在定义的右侧。这不仅是标题中的一行,而且是单独类型的一部分,因此我们可以区分第一个和第二个。生活骇客。对于通用类型(例如您的域实体),请创建文件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
:添加字段isFetching
,Task
(不能为)和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 }
}
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") {
}
return fetch('$githubUrl}/users/saitonakamura/starred')
.then(r => r.json());
}
该配置来自API :GITHUB_URL=https://api.github.com
。githubUrl
API 方法提取出我们上面带有星号的存储库。如果为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为此提供了一个技巧-品牌类型。创建Brand
,B
(字符串)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
但是我们得到一个错误。
现在githubUrl
,nodeEnv
彼此分配是不可能的,因为它们是名义上的类型。不显眼,但名义上。现在我们不能在这里交换它们-我们在代码的另一部分中更改它们。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
}
结论已经准备好了。
结论是类似于我们原本有:true
,false
,Task
。这里有垃圾字段Error
,但类型是undefined
-字段似乎在那里,但似乎没有。第四提示
不要劳累。
如果您放松并潜水得太深,则可能没有足够的氧气返回。培训还包括:如果您过于沉迷于技术,并决定将其应用到任何地方,那么很可能会遇到错误,这是您不知道的原因。这将导致拒绝,并且将不再希望使用静态类型。尝试评估自己的力量。TypeScript如何减慢开发速度
支持键入将需要一些时间。它没有最好的UX-有时它会给出完全无法理解的错误,就像其他类型的系统一样,例如Flow或Haskell。系统表现力越高,错误越困难。
类型系统的价值在于它可以在发生错误时提供快速反馈。系统将显示错误,并花费更少的时间查找和纠正错误。如果您花费更少的时间纠正错误,那么更多的体系结构解决方案将受到更多关注。如果您学习使用类型,它们不会减慢开发速度。++. - , , (25 26 ) - (27 — 10 ).
(5900 ), ++ IT-, .
AvitoTech FrontendConf 2019 . youtube- telegram @FrontendConfChannel, .