从动态类型到静态类型只需三步
TLDR;直接看结论。
我们听说,一个强大的静态类型系统可以减少应用程序中的错误数量,将凌晨两点的生产环境问题变成文本编辑器里的一个红色波浪线。这确实很吸引人。
在这篇文章中,我们将首先给出一些定义、一个场景和一个目标,然后看看这段小冒险会如何发展。最后,我们将尝试得出一些结论。
动态和静态分别是什么意思?
- 动态类型系统是指在运行时检查类型的系统。
- 静态类型系统是指在编译时检查类型的系统。
设想
假设我们的代码需要一个简单的函数来返回数组的最后一个元素(我们称之为“ last”)。
进球🏁
我们的目标是建立一个系统,当我们尝试使用数组以外的任何参数调用此函数时,系统会发出警告;同时,系统还会确保我们的函数接受数组作为输入,并返回一个元素(如果数组为空,则返回错误)作为输出。
这是我们希望达到的行为:
last([ 1, 2 ]) // Should return 2
last([ "1", "2" ]) // Should return "2"
last([]) // Should return some kind
// of error, because an
// empty array does not
// have a last element
相反,类型系统不应该允许这些调用:
last() // Should not be allowed
last(42) // Should not be allowed
last("42") // Should not be allowed
last(null) // Should not be allowed
last(undefined) // Should not be allowed
1. JavaScript 作为入门
我们先从 JavaScript 开始。以下是我们的简单函数:
const last = (arr) => arr[ arr.length - 1 ]
这是调用该函数的结果,PASS并FAIL符合我们上面提到的目标要求。
last([1,2]) // PASS: 2
last(["1","2"]) // PASS: "2"
last([]) // PASS: undefined
last() // FAIL: Crash
last(42) // FAIL: undefined
last("42") // FAIL: "2"
last(null) // FAIL: Crash
last(undefined) // FAIL: Crash
我们得到了 3 个通过和 5 个失败的结果。即使我们发送的值不是数组(例如 `int`42和 `int` "42"),JavaScript 也会尽力保持脚本运行。毕竟,它们都会产生某种结果,所以为什么不行呢?但是对于更极端的类型(例如 `int`null或 `int` undefined),弱类型的 JavaScript也会失败,并抛出一些错误:
Uncaught TypeError: Cannot read properties
of undefined (reading 'length')
Uncaught TypeError: Cannot read properties
of null (reading 'length')
JavaScript 缺乏在脚本执行前发出潜在错误警告的机制。因此,如果我们的脚本没有经过充分测试,就可能在凌晨 2 点直接在用户的浏览器中崩溃……尤其是在生产环境中。
2. TypeScript 来救场了
TypeScript 是 JavaScript 的超集,因此我们可以重用之前编写的相同函数,并从一个宽松的设置开始,看看 TypeScript 开箱即用的功能。
目前我们看到的区别是,last不带参数调用函数的结果从 JavaScript 中的应用程序崩溃变成了 TypeScript 中的这个错误:
Expected 1 arguments, but got 0.
这是一个改进!其他所有行为保持不变,但我们收到了一条新的警告:
Parameter 'arr' implicitly has an 'any' type,
but a better type may be inferred from usage.
TypeScript 似乎尝试推断此函数的类型,但未能成功,因此使用了默认值any。在 TypeScript 中,any这意味着一切照旧,不进行任何类型检查,类似于 JavaScript。
以下是 TypeScript 推断出的类型:
last: (arr: any) => any
让我们告诉类型检查器,我们希望这个函数只接受数字数组或字符串数组。在 TypeScript 中,我们可以通过添加类型注解来实现这一点number[] | string[]:
const last = (arr: number[] | string[]) =>
arr[ arr.length - 1 ]
我们也可以使用Array<number> | Array<string>代替number[] | string[],它们是同一个意思。
现在的情况是这样的:
last([1,2]) // PASS: 2
last(["1","2"]) // PASS: "2"
last([]) // PASS: undefined
last() // PASS: Not allowed
last(42) // PASS: Not allowed
last("42") // PASS: Not allowed
last(null) // FAIL: Crash
last(undefined) // FAIL: Crash
这是一个巨大的进步!6人通过,2人不及格。
我们仍然遇到一些问题null。undefined是时候赋予 TypeScript 更强大的功能了!让我们启用这些标志。
noImplicitAny- 启用对包含隐含any类型的表达式和声明的错误报告。之前我们只会收到警告,现在应该会收到错误报告。strictNullChecks- 将使它们具有不同的类型null,undefined以便如果我们尝试在需要具体值的地方使用它们,就会出现类型错误。
好了!最后两个条件现在满足了。调用该函数时,无论使用哪个参数null,undefined都会产生错误。
Argument of type 'null' is not assignable
to parameter of type 'number[] | string[]'.
Argument of type 'undefined' is not assignable
to parameter of type 'number[] | string[]'.
.D.TS让我们来看一下类型注释(通常情况下,当您将鼠标悬停在函数名称上或使用在线 Playground 时查看选项卡时,可以看到它)。
const last: (arr: number[] | string[]) =>
string | number;
这似乎有点奇怪,因为我们知道,undefined即使传入last一个空数组,该函数也会返回,因为空数组没有最后一个元素。但是推断出的类型注解却表明,该函数只返回字符串或数字。
如果我们忽略该函数可能返回未定义值这一事实而调用它,就会产生问题,使我们的应用程序容易崩溃,而这正是我们试图避免的。
我们可以通过为返回值提供显式类型注解来解决这个问题。
const last =
(arr: number[] | string[]): string | number | undefined =>
arr[ arr.length - 1 ]
我最终发现还有一个标志位可以实现这个功能,它叫做 ` noUncheckedIndexedAccess.`。将此标志位设置为 true 后,类型undefined将自动推断,这样我们就可以回滚最近添加的内容。
还有一点。如果我们想用这个函数处理布尔值列表呢?有没有办法告诉这个函数任何类型的数组都可以?(这里的“any”指的是英文单词“any”,而不是TypeScript类型any)。
我们来试试泛型:
const last = <T>(arr: T[]) =>
arr[arr.length - 1]
现在它能正常工作了,boolean而且可能也接受其他类型。最终的类型注解是:
const last: <T>(arr: T[]) => T | undefined;
注意:如果您在使用泛型时遇到错误,例如,Cannot find name 'T'可能是由 JSX 解释器引起的。我认为它误将 JSX 解释器识别为HTML。在在线 Playground 中,您可以通过在 JSX 解释器中<T>选择 JSX 解释器来禁用它。noneTS Config > JSX
严格来说,这里似乎仍然存在一个小问题。如果我们last这样调用:
last([]) // undefined
last([undefined]) // undefined
即使调用函数时使用的参数不同,我们仍然得到相同的值。这意味着,如果last函数返回undefined空值,我们不能百分之百确定输入参数是一个空数组,它也可能是一个末尾包含未定义值的数组。
但这已经足够好了,所以我们就接受这个方案作为最终解决方案吧!🎉
要了解更多关于 TypeScript 的信息,您可以在官方文档网站上找到优秀的资料,或者您可以在在线 Playground 中查看本文的示例。
3. Elm 用于 typed-FP 体验
使用函数式语言实现相同目标的体验如何?
让我们用 Elm 重写这个函数:
last arr = get (length arr - 1) arr
这是调用该函数后所有情况下的结果:
last (fromList [ 1, 2 ]) -- PASS: Just 2
last (fromList [ "1", "2" ]) -- PASS: Just "2"
last (fromList [ True ]) -- PASS: Just True
last (fromList []) -- PASS: Nothing
last () -- PASS: Not allowed
last 42 -- PASS: Not allowed
last "42" -- PASS: Not allowed
last Nothing -- PASS: Not allowed
所有测试都通过了,所有代码都通过了类型检查,一切都按预期运行。Elm 能够正确推断所有类型,我们无需向 Elm 编译器提供任何提示。目标达成!🎉
last那么上面提到的“咬文嚼字”问题呢?以下是使用[]和调用 的结果[ Nothing ]。
last (fromList []) -- Nothing
last (fromList [ Nothing ]) -- Just Nothing
太好了!我们得到了两个不同的值,现在就可以区分这两种情况了。
出于好奇,推断出的类型注解last是:
last : Array a -> Maybe a
要了解更多关于 Elm 的信息,官方指南是最好的起点,或者您也可以在在线游乐场中查看此帖子的示例。
结论
这个例子只涵盖了类型系统的某些方面,因此远非详尽的分析,但我认为我们已经可以推断出一些结论。
JavaScript
纯 JavaScript 缺乏在执行前发出错误警告的能力。它非常适合构建原型,因为我们只需要关注正常流程,但如果我们需要可靠性,最好不要使用纯 JavaScript。
TypeScript
TypeScript 是一款强大的工具,旨在让我们能够无缝地应对 JavaScript 这种高度动态语言的特性。
在弱类型动态语言的基础上添加静态类型,同时保持其超集地位,并非易事,而且会带来一些权衡取舍。
TypeScript 允许某些在编译时无法确定其安全性的操作。当类型系统具有这种特性时,就被称为“不健全的”。TypeScript要求我们编写类型注解来帮助推断正确的类型。TypeScript 本身无法证明其正确性。
这也意味着,有时需要与 TypeScript 编译器进行斗争才能把事情做好。
榆树
Elm 从一开始就采取了不同的方法,摆脱了 JavaScript 的束缚。这使得 Elm 能够构建一种拥有符合人体工程学且连贯的类型系统的语言,而这种类型系统是直接内置于语言本身的。
anyElm 类型系统是“健全的”,所有类型在整个代码库中都经过验证是正确的,包括所有外部依赖项( Elm 中不存在“不存在”的概念)。
nullElm 的类型系统还处理缺失值和错误等额外事项,因此不需要`int` undefined、` throwint` 和`int`的概念try/catch。Elm 还内置了不可变性和纯函数性。
Elm通过这种方式保证不会出现运行时异常,使我们无需承担查找所有可能出错情况的责任,从而可以专注于编码的其他方面。
在 Elm 中,类型注解完全是可选的,推断出的类型总是正确的。我们不需要向 Elm 的类型推断引擎提供任何提示。
因此,如果 Elm 编译器报错,则意味着客观上类型存在问题。
Elm就像一个优秀的助手,他做事从不问问题,但当我们犯错时,他会毫不犹豫地指出。
标题插图源自Pikisuperstar的作品。
文章来源:https://dev.to/lucamug/ Three-steps-4n7