mobx-keystone
Version:
A MobX powered state management solution based on data trees with first class support for TypeScript, snapshots, patches and much more
112 lines (98 loc) • 3.52 kB
text/typescript
import { getTypeInfo } from "../getTypeInfo"
import { resolveStandardType, resolveTypeChecker } from "../resolveTypeChecker"
import type { AnyStandardType, AnyType, TypeToData } from "../schemas"
import { lateTypeChecker, TypeChecker, TypeInfo, TypeInfoGen } from "../TypeChecker"
import { TypeCheckError } from "../TypeCheckError"
/**
* A refinement over a given type. This allows you to do extra checks
* over models, ensure numbers are integers, etc.
*
* Example:
* ```ts
* const integerType = types.refinement(types.number, (n) => {
* return Number.isInteger(n)
* }, "integer")
*
* const sumModelType = types.refinement(types.model(Sum), (sum) => {
* // imagine that for some reason sum includes a number 'a', a number 'b'
* // and the result
*
* const rightResult = sum.a + sum.b === sum.result
*
* // simple mode that will just return that the whole model is incorrect
* return rightResult
*
* // this will return that the result field is wrong
* return rightResult ? null : new TypeCheckError(["result"], "a+b", sum.result)
* })
* ```
*
* @template T Base type.
* @param baseType Base type.
* @param checkFn Function that will receive the data (if it passes the base type
* check) and return null or false if there were no errors or either a TypeCheckError instance or
* true if there were.
* @returns
*/
export function typesRefinement<T extends AnyType>(
baseType: T,
checkFn: (data: TypeToData<T>) => TypeCheckError | null | boolean,
typeName?: string
): T {
const typeInfoGen: TypeInfoGen = (t) =>
new RefinementTypeInfo(t, resolveStandardType(baseType), checkFn, typeName)
return lateTypeChecker(() => {
const baseChecker = resolveTypeChecker(baseType)
const getTypeName = (...recursiveTypeCheckers: TypeChecker[]) => {
const baseTypeName = baseChecker.getTypeName(...recursiveTypeCheckers, baseChecker)
const refinementName = typeName || "refinementOf"
return `${refinementName}<${baseTypeName}>`
}
const thisTc: TypeChecker = new TypeChecker(
baseChecker.baseType,
(data, path, typeCheckedValue) => {
const baseErr = baseChecker.check(data, path, typeCheckedValue)
if (baseErr) {
return baseErr
}
const refinementErr = checkFn(data)
if (refinementErr === true || refinementErr == null) {
return null
} else if (refinementErr === false) {
return new TypeCheckError(path, getTypeName(thisTc), data, typeCheckedValue)
} else {
// override typeCheckedValue
return new TypeCheckError(
refinementErr.path,
refinementErr.expectedTypeName,
refinementErr.actualValue,
typeCheckedValue
)
}
},
getTypeName,
typeInfoGen,
// we cannot check refinement here since it checks data instances, not snapshots
(sn) => baseChecker.snapshotType(sn),
(sn) => baseChecker.fromSnapshotProcessor(sn),
(sn) => baseChecker.toSnapshotProcessor(sn)
)
return thisTc
}, typeInfoGen) as any
}
/**
* `types.refinement` type info.
*/
export class RefinementTypeInfo extends TypeInfo {
get baseTypeInfo(): TypeInfo {
return getTypeInfo(this.baseType)
}
constructor(
thisType: AnyStandardType,
readonly baseType: AnyStandardType,
readonly checkFunction: (data: any) => TypeCheckError | null | boolean,
readonly typeName: string | undefined
) {
super(thisType)
}
}