基于TS 等语言集合、类型和类型检查的研究

文摘   2024-10-31 13:03   江苏  

集合、类型和类型检查

这篇文章的第一部分解释了类型检查存在的原因以及类型的抽象能做什么。在对类型理论进行了一些辩护之后,我们将深入探讨类型是什么以及常见构造的细节。在最后,我们将把定义付诸实践,并解释包括类型操作在内的类型检查的实现。

类型提供信息

在深入细节之前,我认为最好解释一下为什么理论存在,以回应那些询问类型理论要点的人? 最简单的说法是:类型提供了关于表达式结构、程序的哪些部分作为输入以及哪些部分产生输出的信息。

对于无效程序中的错误信息

“类型”的主要关联是在类型检查中。一旦程序类型得到解决,每个单独的类型就可以在其他类型之间进行比较,并使用值来检查程序是否健全。

例如,在这里我们发现一个数字正在乘以一个字符串。在这个例子中,字符串的乘法没有定义,所以我们的程序存在问题。关于这个类型的信息可以在运行时显示,或者像下面这样提前显示。无论哪种方式,表示表达式的类型都有助于发现程序声明中的问题。

对于构建优化的信息

我这里不会太多涉及,但类型信息对于做出决策以使程序运行时开销更小非常有用。例如,在C语言中,提供关于数字的类型信息可以指示CPU使用更优化的寄存器。

还有一些更玄妙的行为,比如Rust中的可变别名。但我这里不会太多涉及优化,而是专注于类型的检查方面。

为程序员提供信息

人类喜欢对事物进行分类,类型在程序和代码中提供了一种这样做的方式。并非所有东西都能从额外的语法中受益,但对于基于数据的问题,先写出类型再写逻辑可以帮助更好地结构化程序,并了解需要完成什么。当我们回到代码时,我们可以寻找关键的数据结构,从那里看看方法是如何连接的。比如给函数添加返回类型可以帮助理解函数大致做什么,而无需深入到完整的定义中。

类型也有助于理解不熟悉的东西。无论是标准库还是外部库,提供类型信息都有助于了解一些东西。例如,Rust的std.rs(以及个别库的docs.rs)是结构体、枚举和特征的百科全书,这些都包含在类型信息的范畴内。

此外,类型有助于代码的变更。如果我们更改了一个函数,那么我们可以看到这个变化如何影响到依赖它的代码。此外,虽然变更日志很有帮助,但这种抽象对于升级第三方库来说更加机械化。

我不会完全深入到类型的人体工程学,但我认为这是一个好的点,类型不仅仅是为了机器。这也意味着类型应该是易于访问的,与它们相关的问题应该是用户可以理解的。

类型并不是抽象程序的唯一组成部分,其他属性如中间表示(IR)、控制流图、具体和抽象语法树等部分都为程序合成(程序合成 = 机器理解程序的作用)的不同部分提供帮助。

什么是类型?

类型描述数据。数据代表事物、属性、从数据到其他数据的映射、关于事物之间关系的信息。类型允许我们对数据进行分类,这种辨识和分组允许我们进行推理。

这种分类是基于属性进行的。在这里,我以抽象的意义使用“属性”这个词,指的是数据的一些可测量的东西,数据在系统其余部分的工作方式。(这是一种对编程语言中的字段和数据成员的扩展)。

一些现实世界的属性可能是:

  • 是红色的
  • 能灭火
  • 是一辆消防车

这些属性中的每一个都是针对语言量身定制的。例如,一些语言有“符号”,这基本上是程序的唯一值。我们可以定义一个需要确切符号的类型。同样,其他语言有异步函数的概念,这个属性可以编码到类型的功能中。在Rust中,我们可以有一个函数,要求输入是类型&'static str,这些是在程序的整个生命周期内有效的字符串切片。我们也可以有限制,比如类型实现Display,这意味着它可以打印给用户看。

将属性编码为类型提供了推理程序整个生命周期中数据是什么的信息,因此在上面的例子中可以帮助提前发现问题并优化程序。

类型与集合略有不同,它们基于构造而不是谓词。构造指的是在给定一些输入的情况下构建某物。我们有一些数据,我们可以通过一些构造函数形成一个类型的成员。我将在以后的文章中讨论这些差异。但在大多数情况下,集合可以被视为等同于类型,具有相同的规则和用途。

条目

在世界上存在着属于这些属性的实体:

  • 西红柿、YouTube标志和🌉都是红色的
  • 二氧化碳和🧯可以灭火
  • 🚒是一辆消防车

说一个值满足属性通常被称为类型判断,我们通常写成冒号形式。例如,⊢ 🚒 : 消防车,或者在编程语言中 ⊢ 5 : number。

注意你会在类型理论中经常看到冒号:

关于条目的事实

像集合一样,类型可以通过描述拥有无限多的不同条目。然而,当涉及到在确定性图灵机(计算机)上的程序时,由于内存的限制,只能表示有限数量的数字。得益于指数组合,尽管是有限的,但即使只有8字节的内存,你仍然可以覆盖非常大的数字范围。

在编码这类类型时,我们可以将它们视为各个成员的集合。然而,将u64的成员存储在某个列表中将导致147,000,000TB的数据量,我个人无法计算出遍历所有数据条目并比较每个条目是否对程序有效所需的CPU年数。因此,对于这类类型,更容易将u64表示为一个无符号的(没有负值,零字节是数字零)且宽度为64位的数字类型。有了这些信息,它涵盖了所有条目,我们可以更有效地推理值,而不是查询每个条目。

有了条目,我们就有了一个有效判断的内涵。如果我们有x = y且x:数字,那么我们可以得出结论y是一个数字。这通常是你在定义相等时想要维护的一个属性。等式操作符应该测试所有属性是否相等,因此我们可以说它们具有相同的类型。

此外,条目的最后一个随机事实是:对于具有更多属性的类型(因此一般来说,具有更多属性的类型的条目更少)更难找到条目。

基本类型:any和never

两个最基本的类型是any和never。它们是彼此的对立面。

any可以被认为是一个没有任何单一所需属性的类型。我们可以说消防车、勺子或字符串“Hello World”是any事物的元素,因为没有任何属性需要满足。(如果有帮助,这是在属性数量为空的情况下的vacuous truth的一个案例)。因此,any包含了你可以构造的所有值。

any存在于TypeScript中,但在某些情况下行为略有不同(稍后解释)。但它不仅仅是动态类型语言的属性,Rust也有一个any类型,它的工作方式略有不同,但用于通用数据。

另一方面,never具有所有可能构造的属性。要成为一个never项,就必须同时在伦敦和月球上,并且是一种颜色,是蓝色、绿色和紫色。因为需要每个属性,所以将存在两个互斥的属性。(这将在后面介绍)。你不能同时是一种颜色和蓝色以及绿色。

因此,我们可以说你无法通过5、“Hello World”或船只来满足impossible。你不能传递5或“Hello World”或船只。

never可以用来表示函数永远不会退出的地方。例如,在无限循环或抛出异常的东西中。它存在于TypeScript中的never和Rust中的never。

稍后将解释这两种类型在类型层次结构中占有非常重要的位置。

我认为never不是一个好的名称。也许impossible或CannotExist是思考这种类型的更好替代名称。我们可以推理它没有条目,因为如果要求不可能的事情,你就会陷入困境。你不能传递5或“Hello World”或船只。

类型的共轭

有两种形式的二元共轭类型及其属性。这里的二元意味着有两个类型被连接。这将产生自己的类型,因此我们可以形成一个类型的递归定义。

对于实现这种类型的定义:你可以将这两种类型表示为其他类型的向量或二叉树。

And类型(集合交集)

And/&类型表示两个类型的交集。这相当于说,这种类型的条目满足左右两边的属性。例如,我们可以将雨林定义为具有连续的树冠和高湿度。要使一个项目被认为是雨林,它必须同时满足具有连续树冠和高湿度的属性。如果这些条件中的任何一个没有得到满足,它就不能被认为是这种类型。

type RainForest = ContinuousTreeCanopy & HighHumidity。

因此,不存在满足never的项目。

这样,就没有可以构造的类型,因为它们都是互斥的。

这种类型的一个定义将是唯一的,但是And类型是对称的。A & B类型等同于B & A。

JavaScript对象是我们拥有交集类型的原因

你可能在想,交集类型的用例是什么?

首先,如果你知道在TypeScript中,对象类型注释只指定了它们必需的属性,并且可以允许条目具有比指定的更多属性。例如,⊢ { a: "hi", b: 4 }: { a: string }。只是它们的定义不需要有确切数量的属性。

许多语言没有交集类型。通常,集合中的交集类型在其他语言中被表示为产品/元组(固定大小的列表)。

例如,[{ a: "hi" }, { b: 4 }]具有与{ a: "hi", b: 4 }等量的数据处理,(它甚至保留了顺序,这是JS对象的一个奇怪属性),但我们希望我们的类型系统不允许在这个项目上使用.push。因此,我们有交集,而不是使用元组来保持与JavaScript相同的语义。

有了交集类型,我们可以构建对象{ a: string } & { b: number } = { a: string, b: number }。虽然

#在这里插入颜色SVG东西

大多数时候,它们以平面形式存储。例如,虽然我们可以将结构定义表示为单属性对象的交集,但将它们组合在一起只会让实现变得更容易,特别是在其他需要考虑类型布局的语言中,需要了解相邻属性的知识。所以,相反,And类型被用来扩展现有类型。如果我们有现有的Type,我们可以用And类型运算符给它增加更多属性:type Extended = Type & { property: string }。

在一些语言中,这可以被认为是一个细化类型。我们有一个基本类型,但我们在左边增加了额外的必需属性。

减少交集类型

交集类型有一个。A & A = A,所以如果我们尝试用相同的类型构建一个交集,我们可以只返回初始结果。此外,never & A总是never。虽然可能有A的条目,但我们没有任何never的条目,所以nothing存在于never & A中,因此它等同于never。

此外,我们还可以做一些其他的简化(这些包括稍后将介绍的操作)。首先,如果A是B的子类型,我们可以将“较小”的类型(子类型)考虑在内:A & B = A。例如,4 & number = 4。此外,如果类型A与B不相交,我们可以将结果视为never:A & B = never。例如,number & string = never。

为什么与any的交集总是any

对角线恒等式

这里的一个重要点是当any & X被考虑时。从集合的角度来看,它应该表明any始终是超类型(大于X),所以any & X始终是X。然而,TSC在某些地方对any有不同的含义,实际上也将其视为子类型(稍后介绍)。因此,对于TypeScript,any & X实际上是any。我不确定子类型重用是否是因为这个结果,或者是否在这里特别指定,但一般来说,在any的情况下,结果大多是any。

或类型(集合并集或求和类型)

或/|类型表示两个类型的并集。这意味着这个值可以具有左边类型的属性或右边类型的属性。例如,我们可以将火车定义为要么是CargoTrain要么是PassengerTrain,我们可以表示为type Trait = CargoTrain | PassengerTrain。

注意这不是一个互斥或关系。我们可以定义求和类型,如blue | wet,然后海洋是蓝色的,所以它被认为是一个条目,它不一定需要检查wet属性。通常,大多数求和类型是在不相交的属性之间,所以这不是问题。

类似于交集类型,这可以是一个递归关系A | B | C | D。我们可以将其视为一个二叉树列表((A | B) | C)| D。这对于像type TrafficColor = "red" | "amber" | "green"(至少在英国)这样的事物很有用。

等价规则

类似于And类型,有许多等价的表示:

它们是对称的A | B = B | A

满足一个 A | A = A

此外,联合中的never类型可以被移除/折叠。Aka是等价的(因此折叠到左边,因为右边的案例从未被持有)。因此构建类型我们可以只保留never案例A | never = A。

你可以将any视为所有可能类型的并集。使用对称来重新排列和对角线恒等式,我们可以看到A | any = any。

此外,像交集类型一样,我们可以根据不相交和子类型来减少它。如果两个成员不不相交,我们可以选择更大的类型。例如,string | "hi"可以被视为更大的类型string。(有趣的一点是,我们在交集类型中可以选择“较小”的类型(子类型),而在联合类型中我们选择“较大”的类型(超类型))。

Ezno在不进行急切简化或测试子类型时。我发现在某些时候,这比解决的问题更多,只是为了轻微的视觉清理和可能减少每个使用的子类型数量。特别是当你考虑到大型联合比较时的组合规模检查。

标记与未标记求和类型

Rust既有未标记联合union,也有标记枚举。这个标记被称为判别式,并且可以使用同名的判别式函数(在幕后提取标记数据)检索。这个值/标记在每个定义的变体之间是唯一的。

在JavaScript中,每个变体(在优化之前)都有一个标记。你可以认为它是。这不会通过内存暴露,但像typeof和instanceof这样的操作是基于读取这个标记()。在这个粗略的例子中,我们可以认为值的前四位包括它的标记。例如,布尔值是0,数字是1,字符串是2,对象是完全不同的东西,也许在内存中引用了包含它们形状的其他东西。

false = 0b0000_0000 true = 0b0000_0001 5 = 0b0001_0101 "hi" = 0b0010_1010 { a: 1 } = 0b1010_0000

如果你对了解更多关于V8如何表示对象的信息感兴趣,我建议阅读有关V8的博客文章。

空值性和错误处理

求和类型最常见的用途之一是表示可能缺失的数据。例如,我们可以将船帆的颜色表示为Color | null,因为一些船没有帆。

declare function getSailColor(boat: Boat): Color | null

对角线恒等式

今天会下雨吗,或者英格兰永远不会下雨

全局标记

有时候

在Rust中,我们有Option,这相当于T | null

我们可以在null变体上扩展更多关于为什么第一个值没有返回的信息。例如,我们可以在上面的函数上扩展更多信息

declare function getSailColor(boat: Boat): Color | { message: "Boat has no sail", boat_kind: BoatKind }

在Rust中,我们有Result<T, E>,这相当于T | E

(我认为)在Golang中,没有求和类型的的定义。相反,你可以使用每个值都附有一个null成员的事实。相反,要表示像上面这样的错误类型,可以使用这些值的对来代替求和类型。

组合共轭

请注意,这两种类型都遵循布尔代数的根。每个操作都是对称的,重要的是它们分配。

(A | B) & C = (A & C) | (B & C)

用词来说:一个大苹果或香蕉与一个大香蕉或苹果是一样的。

参数化类型

通常称为泛型,用于模拟依赖于某些类型的结构:可以向各种定义中添加类型参数,以指定在某些地方插入类型。这在提供类型模板时非常有用。

这些有时被认为是PI类型或“大类型”,我可能会在以后的博客文章中更多地介绍。

数组是一个与内部类型有关系或在许多类型上工作的类型的示例。虽然我们可以考虑数组的项只是any,但可以通过详细说明各个成员的类型来细化这个类型。

一种方法是为每种类型的数组定义一个类型。

interface StringArray {
    push(a: string);
}

interface NumberArray {
    push(a: number);
}

这有问题,现在我们需要复制定义,每次都在调整相同的字段。同时,这意味着如果我们创建了一个新的类型,如Boat,用户必须为Array创建一个全新的特定定义。

因此,我们使用泛型来表示我们一般类型的模板

interface Array<T> {
    push(a: T)
}

我们稍后将看到,但我们可以通过实例化上述定义来创建上述等效定义:type StringArray = Array<string>; type NumberArray = Array<number>;。我们还可以解决在引入新类型时的问题,因为我们可以得到我们的船只数组Array<boat>

我们可以通过尖括号添加参数。它们可以直接添加到像TypeScript中的interface这样的结构中,也可以直接添加到Rust中的struct和enum中。我们还可以使用类型别名为任何类型添加它们。我们的Rust Option类型可以表示为type Option= T | null。

这些通常用于通用类型,这些类型有点代表自然界事物的修改,而不是存在于自然界本身。例如,Array是一个具有未指定长度的值列表,或者Set是相同的,但没有顺序元数据指定,并且有条件地添加了唯一值,或者Map<K, V>是数据的关联。(后两者可以以Array的形式定义)。

通常使用T作为类型的首字母。从那里开始,额外的字母表上升到U等。对于像Map这样的东西,我们使用K和V分别代表Key和Value。有时使用I作为项目。但我们通常有约定,是一个单字符,并且像大多数类型名称一样,首字母大写。

参数类型

我们可以给我们的类型参数是类型本身。它们有我们可以在注释中引用的名称,使用上述的交集和联合类型,以及以下所有内容。

通常这些类型扩展any。我们可以通过TypeScript中的T extends Type语法和Rust中的T: SomeTrait来改变这一点。例如,对于我们的Set<T>定义,我们可以要求参数T实现等式,可能还有一个哈希操作(等式的启发式)以便集合有效。

部分应用的泛型

关于参数化类型,我们可以用类型参数实例化它们。例如,我们可以有一个数组,但扩展到数组的元素是字符串,使用Array语法。

这个字符串告诉我们,如果我们有一个这种数组类型的表达式,那么获取它的一个元素将返回一个字符串。它还告诉我们更多,例如,push必须接受字符串作为输入,以使我们的数组全部是字符串。它告诉我们传递给.map、.every、.some的函数都必须为字符串定义,以便它们可以针对元素进行评估。

这一点很重要:一个类型参数可能在许多地方使用,包括嵌套在其他类型中,如方法上的函数参数。因此,我们需要小心处理这些泛型,以免根据一个参数创建大量实例。

延迟查找泛型类型

在上述示例中,我们查看了Array类型,我揭示了类型上的更多字段,以展示类型参数如何在不同地方出现多次。

interface Array<T> {
    [item: number]: T; push(t: T); map<U>(mapped: (t: T) => U);
}

当我们有一个引用,如Array时,我们需要产生这种特定类型的Array的内部表示。一种方法是取Array定义并遍历结构,复制每个成员替换任何对T的引用为string。这样的结果将是:

Array<string> = {
    [item: string]: number; push(t: string); map<U>(mapped: (t: string) => U);
}

然而,我们可以加快这一步,而不是形成一个PartiallyAppliedGenerics项目。

一个类型(具有通用参数,如我们原来的未指定的Array)和带有参数的Map(Vec<(TypeId, TypeId)>)。

所以Array<string>变成了PartiallyAppliedGenerics { on: Array, arguments: { T = string } }。这种形式更加高效,因为我们只需要创建一个单独的包装器Type,当我们实例化一个特定的Array版本时,我们不必检查或复制任何内部结构(除了检查它是否具有匹配的参数化和类型参数匹配extends子句)。

我认为这是正确的术语,我们对某些类型进行了部分应用,但没有完全评估一切。

然而,这意味着使用这种类型更加困难。我们不能像它没有用参数专门化那样,简单地访问字段。

PartiallyAppliedGenerics

部分应用修复

所以现在,当访问信息(一个属性,调用一个函数等)时,我们通过解包配对来懒惰地计算它。参数在表结构中收集,称为GenericChain,左侧运行展开。如果找到泛型,查询表(在我正在构建的检查器中,它是用方法.get_argument_covaraint)只需要替换项目。

PartiallyAppliedGenerics包装像泛型接口、继承父项的方法和匿名对象。请注意,类型别名不在此列表中,因为在TypeScript中,对于类型别名(type AliasName= ),实例化它们会立即用RHS替换它们,而不是将它们保留为AliasName

结构上的所有类型参数被认为是使用限制,因此在类型方面是“协变的”。

特殊根类型

类型的乘积/元组/对

我们可以通过乘积形成类型的连接。例如,我们可以为一个带有元组的函数接受一个数字和字符串。这通常在Rust中的括号()或TypeScript中的[]方括号内由逗号分隔的列表定义。

function func(a: [number, string]) {}

乘积与上述JavaScript对象的类型交集不同。它们确实具有等价的属性,但那是未来一篇文章的内容。

元组可以是异构的(不同类型的),而数组通常具有相同的类型(高达求和类型)。在TypeScript中,Array可以是异构的,所以元组不是一个完全不同的类型,而是一种特殊的数组,具有确切长度和确切类型在确切索引处的更多属性。

在集合论中,这被称为笛卡尔积。

对象/结构体是特殊的乘积类型

有了元组,我们通常通过它们的零基索引访问项目。在上面的例子中,如果我们要获取传递的数字数据,我们用a[0](或在Rust中a.0)检索它。

结构体和对象扩展了这一理念,通过字符串键(而不是数字)引用这些索引。在Rust中

struct X { first: number, second: string }

等同于(number, string)类型,但它具有额外的编译时元数据,允许我们用更自然的名字引用字段(你可以看到当字段类型相同时,这特别有帮助)。

在TypeScript中,它继承了对象信息存在于运行时,所以对象稍微复杂一些。它们还允许添加在声明时不存在的属性,因此类型变得更加复杂。

函数

函数是理论中的基本类型。它们代表值列表到其他值的转换。

函数类型抽象了表达式函数的代码定义,主要关注输入和输出是什么。

函数被定义为一系列具有所需类型的参数。输入的类型称为其值域,应用/调用函数的结果是域。

参数是变量。参数是在调用站点传递给函数的值Ahem

泛型参数

类似于泛型类型,函数也可以用泛型参数化。有时这些值来自上方(如Array.prototype.map中的T)。有些是局部的,在调用站点设置。在后面的部分中,我们将看到这些值如何取值以及它们如何影响结果。但目前,它们被视为与具有固定类型的参数处于同一水平。

还有更特别的参数,使事情变得有点棘手。

默认参数

默认参数是用于替换缺少的参数的值

function add(a: number, b: number = 1) { return a + b } add(1, 2) = 1 + 2 = 3 add(1) = 1 + 1 = 2

这相当于经过一些简化后的

function add(a: number, b_before_: number | undefined) { const b = b_before_ ?? 1; return a + b } add(1, 2) = 1 + 2 = 3 add(1, null) = 1 + 1 = 2

出于某种原因,以内联方式编写内部内容与语言一致,并且不应用此转换更简单。

但从这个重写中,你可以看到它的功能如何工作。

请注意,这里参数改变了类型。尽管有注释,但得到的函数参数类型与undefined联合,这是在JavaScript中未传递参数时的值

变长参数

Rest/spread/variadic参数是在调用站点可以采取非固定数量的参数,以便后者被收集到像数组这样的集合类型中

function getLastArgument(…a: Array) { return a.at(-1) } getLastArgument(1, 2) = 2 getLastArgument(1) = 1

这相当于经过一些简化后的

function getLastArgument(a: Array) { return a.at(-1) } getLastArgument([1, 2]) = 2 getLastArgument([1]) = 1

类似于默认参数,特别是因为对于列表的调用站点转换,这不会应用此转换,而是将此编码到函数类型中。

多个参数和currying

我们可以在currying下考虑它来转换

我再次重复自己。用参数列表处理它更接近语言。虽然找到更规范的表示可以帮助简化案例。在这里,我们希望保持更接近语法表示的形式,因为这有助于打印和调用。在像Lean这样的语言中,形式是等效的(后者被简化为前者)。

属性

(a: string) => (b: string) => a + b (a: string, b: string) => a + b

Currying是以伟大的Haskell Curry命名的,他是一位著名的逻辑学家。Curry也很棒。

自由变量和闭包

有时,函数可以使用引用其内部作用域之外的某个东西。当一个变量在上方使用时,它被认为是“自由的”,因为它不受参数的约束。

当这些可以被改变时,可能会出现困难的情况

let b = 2; function add(a: number) { return a + b; } add(1) = 3 b = 6; add(1) = 7

闭包是自由变量的上一层。这是一个返回使用上述作用域中的自由变量的函数。

function closure(a: number) { return function () { return a++; } }

闭包很难,因为它们隐式地存储状态。与对象不同,我们没有为这些数据构建定义。它是由上下文隐式构建的。闭包在基于生命周期的语言中有 问题,因此Rust中存在move关键字。

我认为自由变量是正确的术语。

我将进一步研究闭包,但目前先跳过。

其他函数属性

异步函数可以被视为返回一个Promise(在TypeScript中)或Future(在Rust中)。这些被视为仅对其返回类型进行了轻微修改,而没有其他太大影响。

好了,关于函数带来的复杂性就讲到这里,回到类型理论。

条件类型

我们可以定义类型的属性取决于一个条件。

function func(t: T): T extends string ? "is string" : "not string" { }

这里的类型是T extends string ? "is string" : "not string",条件是T extends string(这是一个稍微滥用的类型判断作为谓词)。

正如我们稍后将看到的,当我们调用这个函数时,我们替换返回类型。在这种情况下,我们针对我们解析的T值评估判断extends string。对于条件,我们可以解决其中一个分支,或者如果这个命题没有明确地分解为truefalse,就解决两者。

这种类型是一个联合类型,具有更多关于结果的信息。如果我们丢弃条件信息,它可以在许多情况下被视为"is string" | "not string"

事实上,有时A | B可以被视为*free condition* ? A : B,其中条件在系统内未知且无法解决。

有时,使用这种类型时,我们可能希望将条件数据向前传递。如果我们在这里索引第一个字符(用item[0]),我们可以得出更明智的T extends string ? "i" : "n"(而不是更弱的"i" | "n")。这有时可以更好地缩小范围。

在Rust中隐含条件的示例

Rust没有条件类型。你可以使用泛型关联类型来模仿这一点,但不是像你那样,你必须为每种可能的输入类型都有实现。由于与impl的实现冲突,不可能像在TypeScript中那样覆盖所有类型,即使有feature(negative_bounds)似乎也不可能。

条件类型在其条件上分配

对于条件类型,一个重要的事情是,如果条件是求和类型,那么它将被分配。

type MyConditional = (A | B) extends C ? X : Y;

立即被解释为type MyConditional = (A extends C ? X : Y) | (B extends C ? X : Y);

这是一个直接的转换,而不是等价类型。这也在泛型替换期间应用。

这种分配增加了(我认为)使一些花式的类型过滤成为可能。将来我会展示另一种可能的方式。

你可以使用未分配的版本,通过将两边包裹在单位元组中。[A | B] extends [Type] ? X : Y;

类型层次结构

  1. ∎ any 包含它下面的一切。
  2. ∎ 这里就是类型类(Rust中称为traits,我认为这是个好名字)存在的地方。它们是由名义类型组成的大型联合。
  3. ∎ 是较小的联合存在的地方,它们对于表示数据非常有用
  4. ∎ 名义类型。布尔使用两个来构建其联合。但是 number(通常是一个64位浮点数,有...成员在联合中)。确切的对象类型可以在这里考虑。
  5. ∎ 对于标签和小量类型很有用。
  6. ∎ 是依赖类型。我们知道它们的所有信息,它们有一个入口。
  7. ∎ never 类型。可以被考虑为上述任何一行的交集。

我没有包括函数、泛型或一些以前看到的类型,只是基础,以看看这个格状表示如何保持

类型上的操作

子类型

在集合类型周围的子类型基本上是在问这个问题:

这个集合适合另一个集合吗?

何时以及为什么需要子类型

在询问如何进行子类型之前,我们需要问何时进行以及为什么需要它。以下是一个带有数字类型参数的函数。

function addOne(a: number) { return a + 1; }

对这个函数的调用只有在它有效替换参数时才有效。

function func(parameter: string) { }

func("argument")

当我们调用这个函数时,我们问的是“argument”是否在字符串集合中(从技术上讲,这个单例类型是一个子类型)。规则将在以后显示,但在这个例子中,很简单,看到这个调用是有效的,因为字符串“argument”具有作为字符串的所有属性。

由此我们知道这个调用是有效的。然而,如果我们有func(4),我们发现类型4没有作为字符串的属性,所以我们将在这里引发类型错误。

子类型问题在许多地方被问到,所以这里有所有子类型发生的地方的长列表。在每种情况下,我已经链接到ezno-checker中使用is_subtype(或包装函数)的地方。

首先是返回类型。当我们返回一个值时,我们检查它是函数声明中指定的注释的子类型。

function func(): number { return "hi"; }

变量初始值(包括默认值)是另一个案例

const x: number = "hi";

分配声明后(包括属性)是上述案例的扩展

let x: number = 4; x = "hi";

let y: { a: number } = { a: 4 }; y.a = "hi";

其他包括:satisfies, Try-catch(ezno-checker特有), 函数重载,泛型参数到类型注释中的参数扩展,This约束(特殊参数案例), 类型基于键(将在以后的博客文章中更多提及), 操作符检查(这使用包装方法)以及上述提到的函数参数到参数。

子类型允许我们推理关于替换或分配是否有效。就像我们不测试每个单独的条目一样,函数参数的子类型允许跳过已经在函数体中完成的所有单独检查。

function func(x: string) { const a: string = x; const b: string = x; const c: string = x; }

func("a")

对参数的单次检查(即不导致4次检查) func("a")

如何进行子类型

子类型是在问一个问题,一个类型是否包含在另一个类型中。我们可以扩展我们的文氏图,通过考虑一个类型被另一个类型覆盖。,你将能够拖动滑块来移动集合。对于覆盖的集合是一个子类型,我们希望在翻译结束时看不到红色。

如果我们没有红色,那么我们就得到了顶部类型是一个子类型。

请注意,这是一种关系,而不是相等。在过渡结束时,我们可以有剩余的黄色。

有时使用符号 _<: _ 来表示这种关系(下划线表示它们可以相等,并且 _: 在类型理论中无处不在)。

X是Y的子类型相当于说X可以分配给Y。

从上面的图表中,我们可以从左上角的案例中得出4可以分配给number,所以4 _<: number,但不是右上角的案例 "hello" _<: number。类似地,在底部行中,从左侧我们发现number _<: number | string,但不是右下角的案例number | string _<: number

子类型是递归的

由于结构化系统,一个关系测试可能会导致许多嵌套的子类型问题。

有时可能会出现子类型最终是循环的情况。在这种情况下,构建了一个“当前检查对”的列表,以便子类型可以退出而不是堆栈溢出。

number string

number

number

number string

详细信息

这里是表格规格

左侧 右侧 结果 也 为什么

类型 X 类型 X 子类型 它们是等价的

原始常量 原始 子类型

常量 同一常量 子类型 相等(与第一种情况相同)

常量 其他 不子类型 只有那个值

A & B X A 子类型 X && B 子类型 X => 子类型

A | B X A 子类型 X || B 子类型 X => 子类型

A X & Y A 子类型 X || A 子类型 Y => 子类型

A X | Y A 子类型 X && A 子类型 Y => 子类型

带类型约束的参数 * 子类型 设置参数

参数 参数 子类型 设置参数

never * 不子类型 空集内无内容

any * 子类型

  • never 子类型

  • any 在 TypeScript 中 不子类型

  • any* 不子类型

函数 函数 下面解释

对象 对象 下面解释

从表中的第一行中,我们可以看到子类型是反射关系。这种类型的事情是子类型的...,我们可以跳过任何类型的检查。

注意,当我们有一个这种关系的左侧的共轭时,它的逻辑遵循它的名字。然而,当它们在右侧时,它被交换了。

函数子类型

有时函数最终出现在这个比较的任一侧。例如参数中的回调

function map(array: Array, cb: number => string): Array{ // ... }

map(() => {})

在这里子类型

类似于标准值,我们还需要检查函数是否也有效传递。

number => string _: 我们的函数

要检查的第一个属性是右侧的函数具有小于或等于相同数量的参数。如果我们的函数比左侧更多的(必需的)参数,这意味着我们可以用缺少数据的方式调用我们的函数。如果我们有更少的,那将是好的,尽管调用带有额外 参数的函数,它不会产生运行时错误,参数简单地被丢弃。

在更严格的Rust中,计数必须相等。

最有趣的事情是:比较参数发生在相反的顺序。如果我们有以下情况。

快速路径

((a: "hi"): number => 2) 满足 (a: string) => number

如果我们以正常方式比较类型的第一个参数,对于其余类型,如 "hi" _<: string,那么我们可以看到它是有效的。然而,这个函数在左手边不是(a: string)=> number,因为它不接受整个字符串集作为输入(或其值域)。

例如,对于以下情况,如果我们以相同的方向进行子类型,则在用参数5调用时会出现错误

const func: (item: string | number) => number = (item: string): number => string.length;

func(5)

这个...的原因是为了围绕传统的子类型操作数。所以我们代替子类型string _<: string | number,这在下面修订的案例中成立。

const func: (item: string) => number = (item: string | number): number => typeof item === "string" ? item.length : item;

func("Test")

这种形式被称为协变和逆变(我认为)

请注意,默认参数和剩余参数的类似过程被保留。

返回类型子类型以正常方式发生。在上述案例中,我们有number _<: number,这很好,因为子类型的反射属性。

对象子类型

当子类型化一个对象时,我们遍历左侧的所有键,并检查

  1. 它存在于右手边的对象中

  2. 它的索引结果具有子类型

以下都是有效的

a: "hi" } _<: { a: string } { a: "hi" } _<: { a: stringnumbera: "hi", b: false } _<: { a: stringnumber

这意味着我们的右手边可以包含右手边没有的属性。(在开始时解释)。这确实会引起一些问题,但那是未来的文章,因为我认为在这些案例中可以取得一些进展。

然而,这些是无效的

{ a: "hi" } _<: { a: boolean } "hi"不是布尔的子类型 { a: "hi" } _<: { a: string, b: string } 缺少必需的属性

在子类型期间收集不匹配项

在上述对象案例中,你可以看到结果不是子类型的一些推理。子类型中有两点要争取

  1. 没有早期返回:尽可能多地收集属性不匹配项(这在后面的文章中进一步解释/证明)

  2. 非二进制输出:了解什么确切的属性缺失是有用的。如果子类型只返回一个bool,那么它将错过可以帮助用户解决这个问题的信息。

将来应该在ezno-checker中更好地打印这个结果。目前一些细节已经存在,但它需要一些修饰

交换修复

在这个过程中还有另一件事被收集,你可以在推理部分阅读。

子类型:名义类型与结构化类型系统

Rust和TypeScript之间的一个关键区别是,在Rust中类型是名义上的。名义类型虽然每个类型都有...,但上述子类型案例被简化为is_subtype = A _<: B,并且不查看A和B定义中的个别属性。

这是因为Rust类型具有布局,因此在替换对象时:属性和判别式值和偏移量都必须对齐。这可以偶尔为类型相等的定义发生,但大多数时候,定义它比展开定义更简单。

而且Rust可以做到这一点,因为他们开始了语言,并且可以添加类型定义的语法,并没有添加通用的对象类型。

在TypeScript作为JavaScript的超集,我们必须处理用户能够创建没有模式的对象,因此需要结构化系统来处理{ a: "hi" }或JSON.parse被用作值。

除了原始类型(string、number、boolean)之外,TypeScript中没有名义类型。如果没有等式快捷方式,子类型必须比较双方的属性以决定值是否可分配。

虽然名义系统更好地定义了值的边界,并且导致较少的边缘情况,但结构化系统可以更简单地使用,特别是在设计的某物中。在Rust中,我经常不得不为少量的类型定义求和类型,这可能是繁琐的,并且可以使用在分配期间展开联合的结构化系统来简化。

然而,在Flow和Ezno中,类是名义上的。

不相交性

与子类型一起,另一个较少提及但同样重要的类型比较函数是不相交性。

虽然子类型测试子集。不相交性是测试交集为空。这相当于说不存在满足这两种类型的元素,或者说这些类型的属性是相互排斥的。

如果你以文氏图的方式考虑这两个,你可以认为两个集合是不相交的,当它们没有重叠时。

我不认为这在手机上现在有效:

不相交 假

属性

实现

类似于子类型,我们可以根据它们的定义解决结果

左侧 右侧 结果 示例

名义类型 A 名义类型 B A != B "hi" ≠ 4

A | B X A与X不相交 && B与X不相交 2 | 4与3不相交

A & B X A与X不相交 || B与X不相交 number & 3与2不相交

对象 * 两个中都有属性具有不相交的值

{ a: string }与{ a: number}不相交

never * 真 ...

any * 假 ...

这里名义与上述相同,可以是一个number、string或常量1、"hi"、false等。对于类型交集和联合,结果最终涉及递归。

类似于子类型右侧,或和与类型使用相反的操作符(这可能是因为它从不相交的角度来框架,如果我用“有一些交集”来反转名称,并且改变了其他案例,那么由于De Morgan身份,结果将基于与输入相同的术语)。

与子类型不同,不相交是对称的(不相交A B = 不相交B A),这就是为什么我只在第一列中放入条目,因为结果仍然可以交换左右列。这就是为什么一些代码看起来重复,但这是考虑双方的最简单方式。

函数和对象在ezno-checker中没有不相交的实现,你可以帮忙添加它们。

不相交关系测试的用途

这种关系在子类型中有一些更玄妙的用途。但它可以帮助解决问题

等式定义良好

大多数编程语言都有等式运算符。在大多数系统中,等式只定义为...。

在JavaScript中,我们有一个开放的书值,等式操作,因为等式总是产生布尔值,不管操作数是什么。

然而,我们可以推断出一些情况,例如一个对象与一个数字比较,{ name: "Josh" } ≠ 4 或甚至 "s" ≠ false(尽管这在ezno-checker中对常量处理得稍微不同)。

这些案例都是由不相交测试决定的。如果它们不不相交(即有一些重叠),那么必须至少有一个元素在两者中,所以这个等式可以是真或假。

另一方面,如果我们发现它们是不相交的,我们知道不存在满足等式的一对,所以我们知道这个表达式总是假。

这种行为扩展到不等。如果我们考虑操作x ≠ y作为!(x ≠ y),我们执行相同的测试(只是诊断打印它总是真的,而不是总是假)。

交集类型定义良好

如果你可以回想一下我们在开始时讨论的一些关于交集类型的规则,我们说如果两者是不相交的,那么它就相当于。

当写一个交集类型引用时,这可能是作者写一个有效组合的意图。所以在这个案例中,警告他们他们的构造是无效的(如果他们想要never类型,他们可以直接引用它)。

type MySpecialType = string & number;

有时你可以在返回类型替换等地方构建一个不相交的类型,这是完全正常和有效的。这个警告只适用于定义。

查找类型别名中的循环

相同的类型

类型别名有点复杂,因为它们必须在有值之前定义(这将在一分钟内在覆盖)。

由于这个原因,一个类型可以被定义为自己的问题。

type X = X;

或mututually type Y = Z; type Z = Y;

这是一个问题,这个类型不是never或any或其他任何类型。它是一个无效的构造,并且没有很好地定义。如果它被很好地定义,那么它可能会破坏。

因此,检查不相交性在类型别名和它的值之间,以捕捉问题。

注意我们不必担心泛型。我们不能形成这种循环type X= T;因为别名总是需要一个参数。最初的问题相当于X<X<X<X<X<...>>>>,我们不能构造。

还有一些边缘情况,仅仅检查不相交性并不能解释。

读取类型的“属性”

子类型和不相交操作很棒。但有时它们不适合我们的一般类型检查。有时我们需要更手动的低级别特定方法来处理类型。

第一个需要更手动处理的地方是对象属性。因为在JavaScript中,对象是属性的集合,我们可以通过访问表达式从它们中获取字段

obj.property 或 obj["property"]

根据类型,我们要么有一个相当直接的时间来计算这个对象属性(例如({ property: string })。属性 string),要么有一个相当困难的时间。

以下是我们需要做一些更多的手动检查(而不仅仅是不相交或子类型操作符)的一些案例

属性访问。上述已覆盖。

获取类型的...方面。稍后详细介绍。

对依赖类型的数字、位、二进制和关系操作的计算

await

instanceof、typeof等。更容易只查看类型,而不是更容易

请注意,子类型和不相交使用属性读取方法来完成子 类型和不相交结论。对于属性访问:我们可以将属性访问定义为子类型{ [property]: T },并返回配对值T的任何值,但你可以看到这变成了1.递归定义和2.比直接查找属性有更多的开销。

读取共轭上的属性

对于交集类型,我们从左右任一侧选择第一个结果(无其他)。对于联合类型,我们必须检查两种类型都有属性。

检索和调用函数

类似于检索属性,获取函数必须以相同的方式查看共轭。

当调用函数时,我们必须处理值参数和类型参数。第一步是处理类型参数(如果存在)。在以下调用中,我们将它们匹配起来(像常规参数一样),这导致类型参数为[(T, number), (U, string)]。

callable

function func<T, U>(a: T, b: U) {}

func<number, string>(1, "hello") = [T = number, U = string]

在任何类型参数之后,我们将值参数与常规参数匹配,并通过子类型检查它们是否有效(上述显示)。如果类型参数在调用站点丢失,那么有一个额外的推理步骤结合到子类型中,将在适当的时候介绍。

类型参数替换

当用泛型调用函数时,输出类型可以根据类型参数。处理完所有值和类型参数后,我们需要为结果以及整个调用表达式提出一个类型,以便我们可以在以下情况下发现错误。

function id<T>(t: T): T { return t }

无法将字符串分配给数字const x: number = id("hello");无法将"hello"分配给数字const y: number = id("hello");稍后介绍 :)

为此,我们取我们的域类型T,我们遍历它的结构(在这个案例中相当简单,因为没有共轭),如果我们发现一个通用类型像T,我们查看我们的参数-参数对数组(在这种情况下是[(T, string)]),如果任何第一对项是类型相等的(具有完全相同的标识符),那么我们替换输出中的类型与第二对项(在这种情况下是string)。

在对象、函数/方法等的情况下,我们可以应用部分应用泛型优化,以便我们不必遍历属性并急切地替换一切。

替换也适用于属性访问,例如array1[4]我们替换T作为数组上的T的值

类型检查过程

从技术上讲,类型检查只是执行上述所有操作(子类型、不相交性、属性检查等)的过程。

对模块/文件的类型检查涉及对所有部分的许多小型类型检查,例如深入单个表达式和单个声明。

为了做到这一点,我们以递归下降的方式遍历AST,从最小的表达式开始。类型检查过程相当复杂,正如我们将看到的,所以我们不使用标准步行者。相反,在步行方面有一些技术细节,将在接下来的步骤中介绍。

它需要在块中进行几次传递

一个表达式需要传递一个预期的类型

一个表达式需要返回一个值类型

上下文

到目前为止,我们一直在讨论随机类型的组合。上下文是系统的结构,它允许我们引用类型并推理系统中的表达式。

上下文存储事物,基本信息基于两个命名空间

变量名到表达式的类型

命名类型到类型注释的类型

function func(parameter: number) {
    // ^^^^^^ 从类型命名空间中获取“number”类型 return parameter // ^^^^^^^^ 从变量命名空间中获取参数类型
}

Rust有第三个用于宏

替换

上下文包含在其他上下文中。我们从根上下文开始。后续上下文具有对它们父上下文的引用。一旦退出作用域,命名空间的内容就不再需要(除非存储用于LSP工作)。

有时一个项目可以有一个变量表示和一个类型表示(例如class有一个通用原型类型和一个变量构造函数类型)

class MyClass {
    constructor(n: number) { this.n = n }
}

const x: MyClass = new MyClass() // ^^^^^^^^ 用作原型类型,相当于{ n: number } // ^^^^^^^^ 用作标记构造函数

这带来了一个令人困惑的事情,变量MyClass不是MyClass的类型。我的类表达式在使用时产生MyClass类型的对象,使用new C(values)语法。

类型与类型注释

每个术语都有一个类型。我们可以为类型添加注释。

根据惯例,许多语言使用尖括号<>而不是像普通函数调用那样的括号来实例化参数。Golang添加了(方括号)。Lean对两者都有一致的语法。

具有用于泛型参数<>和移位运算符<的尖括号分组的语言可能会给解析器带来混乱,这些解析器并不完全了解上下文。

每个术语都有一个可以从上下文中解析的类型。类型注释允许在形式上添加类型信息(以约束的形式)。

与类型注释略有不同,还有以别名和接口形式的类型声明。

这些注释与正常语法的行为不同,因为(在大多数情况下)它们只向编译器表示信息。它们可以完全被擦除(或替换为空白)。

这是这样的事情,+运算符最终会在输出中变成像addsd这样的事情。但你通常找不到数据类型的起源。

反射可以改变这一点。像enum这样的事情在运行时有意义,所以有一些边缘情况,类型...

逐步输入,带有省略注释

类型注释为变量、参数和函数返回类型分配类型。

在是不需要类型注释的语言的超集的语言中。有时类型可能变得冗长且难以编写,并且不适用于非基础工作(演示、实验等),因此系统不需要任何注释。

any被用作缺少注释的类型。因此,它同时作为never和any在子类型中起作用,以便于在已经有类型注释的代码库中采用。

我认为参数约束推理在语言中可以做得更好。Hegel检查器对此有一个很酷的方法,并且有一些即将到来的ezno-checker,我们可以对缺少注释更智能

推理

推理过程通常意味着根据其上下文形成类型。

推断变量约束

在以下情况下,变量a没有约束。我们可以说它的类型是"hello",但这将破坏后面的重新分配,因此我们选择了"hello"的基础,这是string类型。

let a = "hello"; a = "world"

因为没有名义类型,推断这些限制变得有点复杂。例如,虽然完全有效的JavaScript a = 2将不会被允许,因为推断出的约束。

在Ezno中,变量约束和值有特殊的实现,这要归功于对副作用中重新分配的意识。所以这种"推理"不必实现。从这个,系统可以简单地说变量可以重新分配给any,并且可以更灵活的情况(你不应该创建的情况)。然而,在某些情况下,更简单和更现实的(例如for(let x = 0; ...))

推断类型参数

虽然函数可以有泛型参数,但我们不需要在调用站点指定它们。在上面之前显示,但我们可以有。

function identity<T>(t: T): T { return t }

const x: number = identity(5);

在这里我们...,T类型参数是一个number。这是在子类型检查期间完成的,当我们检查T _<: 5时,我们看到T还没有值,所以根据我们的右侧类型5选择一个。

TSC有一个两阶段系统,首先是绑定阶段。在Ezno中,我将两者合并,将其合并到子类型中。

更多关于如何在其他案例中工作的内容将在以后的博客文章中介绍。

推断函数参数类型

当我们在检查阶段扫描表达式和其他AST时,我们可以"传递"一个预期的类型。这允许在没有它们的情况下向参数添加类型。

返回类型"推理"

类似于变量重新分配,我们也可能没有函数的返回类型。有很多方法可以处理这个问题,但在ezno-checker中,基于控制流分析在检查内部块时选择返回类型

function func() { // ^^^ 这里没有返回注释

    return 256
    // ^^^^^^ ^^^ 返回`256`,所以我们将说number是返回类型 } // func: () => number

实际上,在Ezno中,一个"第二"返回类型总是在一个稍微不同的机制下构建的。所以你总是有某种推断返回类型。你可以在这里尝试一下。

反射和缩小/细化类型

我将对我和我的方法进行一个完整的博客文章,但我会给出一个概述。

在JavaScript中,有各种表达式级别的操作允许我们检查类型,例如typeof和instanceof。因为没有模式匹配(👀),根据类型进行条件操作的方式是使用这些表达式。

检查值以获取类型信息被称为反射。当你想到程序告诉你关于它自己的情况时,你可以看到措辞。

function func(param: any) {
    if (typeof param !== "string") { // ^^^^^^^^^^^^^^^^^^^^^^^^^基于这个表达式
        const x: string = param; // ^^^^^我们决定这是string类型 // 在这个条件块内
    }
}

推理

完整的方法涉及在表达式涉及逻辑或时构建联合类型,并在查看逻辑与时进行联合。事情在否定和谓词函数时变得有点复杂,所以我将在以后的博客文章中介绍。

表示类型

所以我们知道我们已经看到了我们如何操作类型。但对我来说最难的事情之一是弄清楚如何在检查器中表示这些。

用Rust编写检查器最初是具有挑战性的,因为受到限制。以下是一些关于类型的考虑。

类型可以是循环的

类型可以是递归的,它们可以具有代表自己的属性。这在Rust中证明是困难的,因为非标记-清除垃圾收集器方法限制了可以表示的内容。

虽然我叫它竞技场, 但它只是一个Vec

最初,我认为一个Rc可以工作,但这可能会导致死锁,其中引用自己的类型不能被释放,因为参考中有循环。虽然这不是CLI中的一个问题,但在LSP(在编辑器中完成的检查)的情况下,这种内存泄漏可能会随着时间的推移使LSP崩溃

竞技场在这里也是一个很好的选择,因为类型在通过后还会持续一段时间。我们知道它们何时不再使用,并且可以手动进行清理。个别计数将涉及大量的增量和减量,这将是一个太细的操作。如果我有一个更高效的类型存储,它也将与模块链接,并基于该模块的生命周期。

在错误上短路并继续

考虑类型检查的一个问题是关于短路。

例如“类型检查”当前文件,我们发现第一个变量声明有问题

function func(): number {
const variable: string = 4; const something: number = "Hello World"return variable
}

现在有一种方式是遍历树,在我们发现第一个问题时返回TypeError。但这将使用这个检查器非常烦人。我们修复问题(在这种情况下,要么RHS表达式不正确,要么可能我们的注释对于这种情况不正确)然后重新运行检查器看看它是否固定。现在它显示了第二个声明const something的问题。

如果这个问题可以在第一次运行时呈现给我们就好了。所以我们可以通过而不是返回错误,我们将错误添加到错误列表中。所以现在在第一次运行中我们得到了三个错误。

在这里有一个决定要关于const variable的类型?我们将其作为字符串类型还是4(即数字)的类型。我们不能根本不定义它,因为变量被定义了。

在这里,我决定将注释:string作为const variable具有的类型(在类型上有标记,表示它是一个错误)。

但在我们不能依赖注释的情况下有案例。

const x: NonExistantType = 2;

在这里我们将未知注释视为any(带有一个标记)

但还有其他时候根本没有注释

const obj = { prop: 1 } const variable = obj.property; // ^^^^^^^^ "property"在{ "prop": number }上不存在

在这里我们将值视为never(带有一个标记)

这也是语法解析的棘手问题。如果我们遇到看起来不正确的语法,比如function func() { = if },我们是将RHS解析为仅表达式还是完整语句?在泛滥用户与一个块的错误,其中一些可能依赖于原始步骤与原始问题之间有一个折衷。其中一些可以通过一个语法错误每文件等来抵消。

虽然在CI中快速失败可能很好,但当您在编辑器中实时工作时,一些语法可能不正确是常见的。

这种对部分有效源的处理是自动完成等所需的。我们实际上需要一个AST来检查,然后我们才能做到这一点let x = ;。将在有关解析的博客文章中介绍。语法错误比类型问题要多得多。

这些功能集目前被称为“稳定性”。

类型与值和未知性

依赖类型

依赖类型被添加到TSC,因为JavaScript没有用于标记联合数据的特定结构。相反,所以很多现有的API使用这些“标记”模式来区分对象。

declare function getUserActivity(): Array<
| { kind: "follow", from: ..., to: ... } | { kind: "comment", on: ..., comment: ... } | { kind: "post", id: ..., caption: ... }

;

const activity: ... if (activity.kind === "follow") {
...
else if (activity.kind === "comment") {
...
else if (activity.kind === "post") {
...
}

这种模式比使用from in activity检查要容易理解得多。

您也可以使用带有instanceof的类联合。但像JSON.parse和Response.prototype.json这样的函数返回没有原型的对象。

const systems

在某些系统中,它们只允许在某些地方使用依赖类型。因为在Rust中,泛型必须特别扩展trait,所以有一个特殊的const修饰符用于参数,这些参数引用类型作为扩展约束,以便这些泛型参数期望参数是值而不是类型。在Rust中const参数用于设置即将推出的SIMD API中的...数量。虽然你可以创建一个struct Eight;等。使用这个const版本意味着你可以使用操作符。

在TypeScript中也有用于泛型参数的const修饰符,它告诉TypeScript在这些情况下更紧密地处理参数。

Zig语言还有一些有趣的编译时处理类型的方法,不幸的是我没有时间在这篇文章中查看。如果你有关于Zig的类型系统特性(特别是在comptime周围)的有趣文章,请在评论中留下。

总结

现在就到这里,希望你喜欢这个大部分高层次的类型概述,并发现一些子类型规则很有趣。

这是深入类型检查的三部曲中的第一篇。在接下来的文章中,我将在这篇文章的基础上,更多地介绍TypeScript的高级部分,以及我自己的类型系统方法。所以请继续关注!


编程悟道
自制软件研发、软件商店,全栈,ARTS 、架构,模型,原生系统,后端(Node、React)以及跨平台技术(Flutter、RN).vue.js react.js next.js express koa hapi uniapp Astro
 最新文章