??
定义Model时,需要正确地定义props中各字段的类型。本文将对MST提供的各种类型以及类型的工厂方法进行简单的介绍,方便同学们在定义props时挑选正确的类型。
前提
定义props之前,有一个前提是,你已经明确地知道这个Model中状态的数据类型。
如果Model用于存放由后端API返回的数据,那么一定要和后端确认返回值在所有情况下的类型。比如,某个字段在没有值的时候你以为会给一个''
,而后端却给了个null
;或者某个数组你以为会给你一个空数组[]
,他又甩你一个null
。如果你不打算自己编写一个标准化数据的方法,那一定要和数据的提供方确定这些细节。
基础类型
types.string
定义一个字符串类型字段。
types.number
定义一个数值类型字段。
types.boolean
定义一个布尔类型字段。
types.integer
定义一个整数类型字段。
注意,即使是TypeScript中也没有“整数”这个类型,在编码时,传入一个带小数的值TypeScript也无法发现其中的类型错误。如无必要,请使用types.number
。
types.Date
定义一个日期类型字段。
这个类型存储的值是标准的Date对象。在设置值时,可以选择传入数值类型的时间戳或者Date对象。
export const Model = types .model({ date: types.Date }) .actions(self => ({ setDate (val: Date | number) { self.date = date; } }));复制代码
types.null
定义一个值为null
的类型字段。
types.undefined
定义一个值为undefined
的类型字段。
复合类型
types.model
定义一个对象类型的字段。
types.array
定义一个数组类型的字段。
types.array(types.string);复制代码
上面的代码定义了一个字符串数组的类型。
types.map
定义一个map类型的字段。该map的key都为字符串类型,map的值都为指定类型。
types.map(types.number);复制代码
可选类型,types.optional
根据传入的参数,定义一个带有默认值的可选类型。
types.optional
是一个方法,方法有两个参数,第一个参数是数据的真实类型,第二个参数是数据的默认值。
types.optional(types.number, 1);复制代码
上面的代码定义了一个默认值为1的数值类型。
注意,types.array
或者types.map
定义的类型自带默认值(array为[]
,map为{}
),也就是说,下面两种定义的结果是一样的:
// 使用types.optionaltypes.optional(types.array(types.number), []);types.optional(types.map(types.number), {});// 不使用types.optionaltypes.array(types.number);types.map(types.number);复制代码
如果要设置的默认值与types.array
或types.map
自带的默认值相同,那么就不需要使用types.optional
。
自定义类型,types.custom
如果想控制类型更底层的如序列化和反序列化、类型校验等细节,或者根据一个class或interface来定义类型,可以使用types.custom
定义自定义类型。
class Decimal { ...}const DecimalPrimitive = types.custom({ name: "Decimal", fromSnapshot(value: string) { return new Decimal(value) }, toSnapshot(value: Decimal) { return value.toString() }, isTargetType(value: string | Decimal): boolean { return value instanceof Decimal }, getValidationMessage(value: string): string { if (/^-?\d+\.\d+$/.test(value)) return "" // OK return `'${value}' doesn't look like a valid decimal number` }});复制代码
上面的代码定义了一个Decimal类型。
联合类型,types.union
实际开发中也许会遇到这样的情况:一个值的类型可能是字符串,也可能是数值。那我们就可以使用types.union
定义联合类型:
types.union(types.number, types.string);复制代码
联合类型可以有任意个联合的类型。
字面值类型,types.literal
字面值类型可以限制存储的内容与给定的值严格相等。
比如使用types.literal('male')
定义的状态值只能为'male'
。
实际上,上面提到过的types.null
以及types.undefined
就是字面值类型:
const NullType = types.literal(null);const UndefinedType = types.literal(undefined);复制代码
搭配联合类型,可以这样定义一个性别
类型:
const GenderType = types.union(types.literal('male'), types.literal('female'));复制代码
枚举类型,types.enumeration
枚举类型可以看作是联合类型以及字面值类型的一层封装,比如上面的性别
可以使用枚举类型来定义:
const GenderType = types.enumeration('Gender', ['male', 'female']);复制代码
方法的第一个参数是可选的,表示枚举类型的名称。第二个参数传入的是字面值数组。
在TypeScript环境下,可以这样搭配TypeScript枚举使用:
enum Gender { male, female}const GenderType = types.enumeration('Gender', Object.values(Gender));复制代码
可undefined类型,types.maybe
定义一个可能为undefined
的字段,并自带默认值undefined
。
types.maybe(type)// 等同于types.optional(types.union(type, types.literal(undefined)), undefined)复制代码
可空类型,types.maybeNull
与types.maybe
类似,将undefined
替换成了null
。
types.maybeNull(type)// 等同于types.optional(types.union(type, types.literal(null)), null)复制代码
不可不类型,types.frozen
frozen
意为“冻结的”,types.frozen
方法用来定义一个immutable类型,并且存放的值必须是可序列化的。
当数据的类型不确定时,在TypeScript中通常将值的类型设置为any
,而在MST中,就需要使用types.frozen
定义。
const Model = types .model('Model', { anyData: types.frozen() }) .actions(self => ({ setAnyData (data: any) { self.anyData = data; } }));复制代码
在MST看来,使用types.frozen
定义类型的状态值是不可变的,所以会出现这样的情况:
model.anyData = {a: 1, b: 2}; // ok, reactivemodel.anyData.b = 3; // not reactive复制代码
也就是只有设置一个新的值给这个字段,相关的observer才会响应状态的更新。而修改这个字段内部的某个值,是不会被捕捉到的。
滞后类型,types.late
有时候会出现这样的需求,需要一个Model A,在A中,存在类型为A本身的字段。
如果这样写:
const A = types .model('A', { a: types.maybe(A), // 使用mabe避免无限循环 });复制代码
会提示Block-scoped variable 'A' used before its declaration
,也就是在A定义完成之前就试图使用他,这样是不被允许的。
这个时候就需要使用types.late
:
const A = types .model('A', { a: types.maybe(types.late(() => A)) });复制代码
types.late
需要传入一个方法,在方法中返回A
,这样就可以避开上面报错的问题。
提纯类型,types.refinement
types.refinement
可以在其他类型的基础上,添加额外的类型校验规则。
比如需要定义一个email字段,类型为字符串但必须满足email的标准格式,就可以这样做:
const EmailType = types.refinement( 'Email', types.string, (snapshot) => /^[a-zA-Z_1-9]+@\.[a-z]+/.test(snapshot), // 校验是否符合email格式);复制代码
引用与标识类型
拿上一篇文章中的TodoList作为例子,我们在对Todo列表中的某一个Todo进行编辑的时候,需要通过id跟踪这个Todo,在提交编辑结果时,通过这个id找到对应的Todo对象,然后进行更新。
这种需要跟踪、查找的需求很常见,写多了也觉得麻烦。
好在MST提供了一个优雅的解决方案:引用类型和标识类型。
这两者需要搭配使用才能发挥作用:
定义标识,types.identifier
标识就是数据对象的唯一标识字段,这个字段的值在库中保持唯一,也就是primary_key。
比如上一篇文章中的TodoItem,可以改造为:
export const TodoItem = types .model('TodoItem', { id: types.identifier, title: types.string, done: types.boolean, });复制代码
使用引用类型进行跟踪,types.reference
改造TodoList:
export const TodoList = types .model('TodoList', { ... list: types.array(TodoItem), editTarget: types.reference(TodoItem), ... });复制代码
然后在创建Model实例,或者applySnapshot的时候,可以将editTarget
的值设定为正在编辑的TodoItem的id值,MST就会自动在list中查找id相同的TodoItem:
const todoList = TodoList.create({ list: [ {id: '1', title: 'Todo 1', done: true}, {id: '2', title: 'Todo 2', done: true}, ... ], editTarget: '1'});//此时的editTarget就是list中id为'1'的TodoItem对象console.log(todoList.list[0] === todoList.editTarget); // truetodoList.editTarget = todoItem2; // todoItem2为id为'2'的TodoItem对象console.log(getSnapshot(todoList).editTarget === '2'); // truetodoList.editTarget = '2' as any;console.log(getSnapshot(todoList).editTarget === '2'); // true复制代码
上面的代码说明,reference类型的字段本质上维护的是目标的标识字段值,并且,除了将目标对象赋值给reference字段外,将目标标识字段值赋值给reference字段的效果是一样的。
另外,reference不仅仅能搭配array使用,也能在map中查找:
const TodoList = types.model('TodoList', { todoMap: types.map(TodoItem), editTarget: types.reference(TodoItem)});复制代码
甚至,MST也允许你自定义查找器(resolver),给types.reference
指定第二个参数,比如官网的这个例子:
const User = types.model({ id: types.identifier, name: types.string})const UserByNameReference = types.maybeNull( types.reference(User, { // given an identifier, find the user get(identifier /* string */, parent: any /*Store*/) { return parent.users.find(u => u.name === identifier) || null }, // given a user, produce the identifier that should be stored set(value /* User */) { return value.name } }))const Store = types.model({ users: types.array(User), selection: UserByNameReference})const s = Store.create({ users: [{ id: "1", name: "Michel" }, { id: "2", name: "Mattia" }], selection: "Mattia"})复制代码
types.identifierNumber
若对象的唯一标识字段的值为数值类型,那么可以使用types.identifierNumber
代替types.identifier
。
types.safeReference
这是一个“安全”的引用类型:
const Todo = types.model({ id: types.identifier })const Store = types.model({ todos: types.array(Todo), selectedTodo: types.safeReference(Todo)});复制代码
当selectedTodo
引用的目标从todos
这个节点被移除后,selectedTodo
会自动被设置为undefined
。
小结
MST提供的类型和类型方法非常齐全,利用好他们就能为任意数据定义恰当的类型。
喜欢本文的欢迎关注+收藏,转载请注明出处,谢谢支持。