Skip to content

定义一个类型

我们来定义一个商品类型。

ts
const Product = typedef({
    name: string,
    price: money,
})

console.log(typeinit(Product))
// output: { name: '', price: '0.00' }
console.log(typeinit(Product, { name: 'Apple' }))
// output: { name: 'Apple', price: '0.00' }

这种由字段组成的结构称为:Struct类型。

其中还使用了金额类型,定义如下:

ts
const money = typedef({
    '@type': string,
    '@value': () => '0.00,
    '@adjust': (self) => {
        // 仅演示,实际使用请注意精度
        return parseFloat(self).toFixed(2)
    },
})

这种包含描述符号@type的称为:修饰类型。@type指向的就是被修饰的类型。
除了基础类型bool,int32,float64,string,unknown,还内置了从unknown修饰来的object,array,和从array修饰来的CArray。 任何类型都可以被修饰,包括Struct类型。

描述符号

修饰类型时,必须提供@type,若没有则表示一个Struct类型。

描述符号说明
@type被修饰的类型
生成值
@value生成默认值
初始化执行顺序 @value > @init
@init自定义初始化,无返回值
只对Struct,unknown有效
校验与修正执行顺序 @verify > @adjust > @assert
@verify对值进行校验,抛出错误
@adjust对值进行修正,返回修正后的值
@assert对值进行断言,抛出错误
引用与释放极少使用
只对Struct,unknown有效
@retain当被父结构体的字段引用时触发
@release当从父结构体的字段释放时触发
其他
@notnil禁止undefined,null
@noinit禁用自动初始化,必须显式使用typeinit来初始化
只对Struct,unknown有效
@change极少使用
标记此类型会触发观察,称为可变类型
只对unknown有效
需使用change()unknownStruct一样触发观察

关于 Struct 类型

Struct类型是工作中最常用到的,我们来掌握几个进阶操作。

修饰须知

  • 不能在修饰中定义新字段,因为修饰不是继承。
  • 修饰后可以使用ruledef定义新规则,但规则名不能重复。

初始化时立刻触发规则

我们知道,只有当字段被赋值时才会触发规则,所以我们巧妙地利用@init来实现。

ts
const Product = typedef({
    price: money,
    // ...
    '@init': (self) => {
        self.price = self.price
    },
})

ruledef(
    Product,
    'price',
    {
        price: true,
    },
    (self) => {
        // ...
    }
)

递归引用

比如实现一个链表结构并不难,但要具备正确的类型提示就要动点脑筋了。

ts
const __LinkedNode = typedef({}) as TypeDesc<unknown>
const _LinkedNode = typedef({
    value: unknown,
    next: __LinkedNode,
}, __LinkedNode as never)

type LinkedNode = typeof _LinkedNode & TypeDesc<Struct<{
    next: LinkedNode
}>>
const LinkedNode = _LinkedNode as LinkedNode

如上,先定义一个空结构体类型,然后再定义字段,就实现了链表结构。接下来重新定义一个递归引用的交叉类型,最后再使用类型断言,就得到正确的类型提示了。

循环依赖

如果有循环引用的 A B 两个类型,分别定义在两个文件中,就会导致循环依赖。这是先有鸡还是先有蛋的问题,需要我们好好发挥聪明才智才行。

我们先创建 A.ts B.ts 两个文件,并分别定义空的结构体类型,这相当于头文件,只声明但不具体实现。然后再创建 A.def.ts B.def.ts 两个文件,在其中定义具体实现。最后在使用 A B 类型前,比如在入口文件中引入一次 A.def.ts B.def.ts 就好了。

ts
import './A.def'
import './B.def'
import { A } from './A'
import { B } from './B'

// 正确的类型提示
typeinit(A).b
typeinit(B).a
ts
import type { A_def } from './A.def'

export const A = typedef({}) as A_def
ts
import { A } from './A'
import { B } from './B'

const __B = B as TypeDesc<unknown>
const __A = A as TypeDesc<unknown>
const _A = typedef({
    b: __B,
}, __A as never)

export type A_def = typeof _A & TypeDesc<Struct<{
    b: typeof B
}>>
ts
import type { B_def } from './B.def'

export const B = typedef({}) as B_def
ts
import { A } from './A'
import { B } from './B'

const __A = A as TypeDesc<unknown>
const __B = B as TypeDesc<unknown>
const _B = typedef({
    a: __A,
}, __B as never)

export type B_def = typeof _B & TypeDesc<Struct<{
    a: typeof A
}>>

Uncaught ReferenceError: Cannot access 'xxx' before initialization

遇到这个错误,大概率是循环引用导致的,需要使用此方法来解决。

反射

极少使用。反射是从结构体实例获取其类型。比如为某个结构体实例定义规则,但该类型未知。此时就是反射的用武之地。

ts
const Delegate = typedef({
    ref: structof(someInstance),
})

ruledef(
    Delegate,
    'someRule',
    {
        ref: {
            // ...
        },
    },
    (self) => {
        // ...
    }
)

typeinit(Delegate, { ref: someInstance })

TIP

此示例同时介绍了一种委托模式。使用委托,我们可以为所欲为。广义上讲,间接引用都算是委托,我们其实一直在用,却不自知而已。

获取字段类型

极少使用。先获取到结构体类型的所有字段,然后从字段名取出该类型。

ts
const User = typedef({
    contact: typedef({
        phone: string,
        email: string,
    }),
})

const Contact = structbody(User).contact

使用数组

默认数组元素的类型都是unknown,若要具有正确的类型提示,需要使用类型断言。

ts
const HomePage = typedef({
    banners: array as TypeDesc<array<typeof string>>,
    products: typedef({
        '@type': array as TypeDesc<array<typeof Product>>,
    }),
})

上面示例,我们使用了两种数组方式:直接使用array和修饰array(建议)。这两种没有区别,都能提供正确的类型提示。但修饰array的好处是,可以使用描述符号@verify来校验数组的元素。

还有一个CArray。两者的区别是,如果定义了规则观察该字段,array类型的索引更新时不会触发规则,而CArray类型的索引更新时会触发规则。因为CArray可变类型,具体细节请阅读源码。

可变类型

极少使用。含有描述符号@changeunknown类型,以及衍生的修饰类型,都是可变类型。配合使用change()可以令unknownStruct一样触发观察。

ts
const HomePage = typedef({
    products: typedef({
        '@type': array as TypeDesc<array<typeof Product>>,
        '@change': true,
    }),
})

ruledef(
    HomePage,
    'products',
    {
        products: true,
    },
    (self) => {
        console.log(self.products.length)
        // ...
    }
)

const { products } = typeinit(HomePage)
products[0] = typeinit(Product)
change(products) // output: 1

上面示例也可以换用CArray来实现,其原理就使用了change()Proxy,所以当索引更新时会自动触发规则。

MIT Licensed