Skip to main content

常用类型

基本类型

RowScript 支持并兼容以下 JavaScript 基本类型:

内置操作符

RowScript 不支持所谓 JavaScript 中著名的 “感谢发明了 JavaScript” 梗,使得操作符的功能过于“强大”。比如,+/- 操作符仅适用于类型相同的参数。

1 + 2 // ✅

1 + "123" // ❌

Unit 类型

在 RowScript 中,我们决定不采用 nullundefinedvoid,原因是它们一旦混合使用便容易引发混乱。作为替代,我们引入了一种名为 unit 的新基本类型。这种类型与 void 类似,不过区别在于用户实际上可以声明一个此类型的变量,这一特性让它独树一帜。

可以使用 () 创建 unit 类型的值,我们称之为“unit value”。

const a: unit = ();

在将代码编译到 JavaScript 时,unit value 会被转换为 undefined 以保证兼容性。

函数类型

函数类型(function type)和 TypeScript 相似,拥有相同的语法:

const first: (x: number, y: number) => number = (x, y) => x;
// ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^
// 此处即为函数类型的表达式

在函数类型的表达式中,函数的参数名字是必须要填写的,这点和 TypeScript 一致,这是因为参数名字能起到文档注释的效果,参数名称不同并不会导致函数类型的不同。

对象类型

对象类型(object type)和 TypeScript 同样类似,可以通过直接的方式表达一个对象类型:

function f(): {n: number} {
{n: 42}
}

也可以搭配类型别名使用:

type Data = {n: number};

function f(): Data {
{n: 42}
}

属性访问

访问一个属性(property)和 JavaScript 相似,使用 . 语法即可:

const a = {n: 42};
const b = a.n;

需要注意的是,假设一个对象的属性是一个函数,那么属性访问后的直接调用是不合法的:

const a = {f: () => 42};

a.f(); // ❌ invalid

(a.f)(); // ✅ okay

这么设计的原因有两个:

  1. 推荐将所谓的“方法(method)”声明在 中,使用类来对数据和方法统一排布
  2. 由于 “点”语法(dot syntax)的特殊性,f 在这里可能会是全局的函数,可能是类的方法,也可能是对象的对象,显式的括号能够起到一个强调“对象的属性”的作用,给读者提供了很好的提醒效果

对象拼接

不同的对象可以进行拼接(concatenation),拼接中如果出现相同的属性,即使两个属性的类型不同,但是拼接操作左边的对象 会被右边的对象覆盖

使用操作符 ... 拼接两个不同的对象。

function f(): {n: string} {
const a = {n: 42};
const b = a ... {n: "hello"};
b
}

const a = f(); //=> {n: "hello"}

向下转换

对象可以进行向下转换(downcast),即将一个“更大”的对象转换成一个“更小”的对象,去掉多余的属性,保留用户所选的属性。

使用 {...expr} 语法对对象进行向下转换。

function f(): {n: number} {
const a = {m: "hello", n: 42};
{...a}
}

const a = f(); //=> {n: 42}

枚举类型

枚举类型(enumeration type)类似 TypeScript 中的 union 类型,但是额外添加了 union 下每个类型的标签(tag,又称作 discriminant)。

一个枚举类型的值被称为 variant,一个 variant 是其标签和具体数值的结合体。用户可以用 大驼峰风格 的标识符创建一个 variant,例如:

const a = Hello;

值得注意的是,此时 a 的类型为 [Hello],即当前只有 Hello 这一个标签的情况。当 variant 的数值类型没有指定时,类型默认为 unit,所以 [Hello] 实际上是 [Hello(unit)] 的语法糖。以上代码生成的 JS 结果为:

// JS
const a = {Hello: undefined};

可以用类似函数调用的方式给 variant 加上具体的数值,例如:

const a = SomeValue(42);

此时 a 的类型为 [SomeValue(number)],生成的 JS 代码为:

// JS
const a = {SomeValue: 42};

从技术的角度说,RowScript 的枚举类型和 Rust 的枚举类型,以及 PureScript、Elm、Haskell 等语言的 ADT(algebraic data type)不同的地方在于,RowScript 枚举类型是 可以任意扩展的,这点来说,它和 OCaml 语言的 polymorphic variant 十分之相似。

那我们应该如何任意扩展呢?

向上转换

我们可以通过向上转换(upcast)的方式扩展一个枚举类型。向上转换和向下转换相反,我们将一个“更小”的 variant 向“更大”的 variant 进行转换。

使用 [...expr] 语法进行向上转换:

const a: [SomeValue(number) | NoValue] = [...NoValue];

可以观察到 NoValue 的类型为 [NoValue],通过向上转换后转换成了 [SomeValue(number) | NoValue] “更大”的枚举类型。

由于向上转换是十分常用的操作,所以向上转换被做成了隐式(implicit)的,即一个 variant 的向上转换是完全自动的。以下代码都是合法的:

const a: [Foo] = Foo;
const b: [Foo | Bar] = a;
const c: [Foo | Bar | Baz] = b;

Switch

一个 variant 可以通过 switch 表达式进行值匹配。

function f(a: [SomeValue(number) | NoValue]) {
switch (a) {
case SomeValue(n):
console.log(n);
case NoValue:
console.log("no value");
}
}

当 switch 缺少一个标签的 case 时,会产生编译错误。

function f(a: [SomeValue(number) | NoValue]) {
switch (a) {
case SomeValue(n):
console.log(n);

// ❌ invalid,缺少 NoValue 分支。
}
}

使用默认分支(default branch)来覆盖其他可能的情况:

function f(a: [SomeValue(number) | NoValue]) {
switch (a) {
case SomeValue(n):
console.log(n);
default:
console.log("no value");
}
}

数组类型

TODO

Map 类型

TODO