mobx-keystone
Version:
A MobX powered state management solution based on data trees with first class support for TypeScript, snapshots, patches and much more
190 lines (161 loc) • 5.35 kB
text/typescript
import { failure, lazy } from "../../utils"
import { getTypeInfo } from "../getTypeInfo"
import {
resolveStandardType,
resolveStandardTypeNoThrow,
resolveTypeChecker,
} from "../resolveTypeChecker"
import type { AnyStandardType, AnyType } from "../schemas"
import {
getTypeCheckerBaseTypeFromValue,
lateTypeChecker,
TypeChecker,
TypeCheckerBaseType,
TypeInfo,
TypeInfoGen,
} from "../TypeChecker"
import { TypeCheckError } from "../TypeCheckError"
import { typesUnchecked } from "./typesUnchecked"
/**
* A type that represents the union of several other types (a | b | c | ...).
* Accepts a dispatcher that, given a snapshot, returns the type
* that snapshot is.
*
* @template T Type.
* @param dispatcher Function that given a snapshot returns the type.
* @param orTypes Possible types.
* @returns
*/
export function typesOr<T extends AnyType[]>(
dispatcher: (sn: any) => T[number],
...orTypes: T
): T[number]
/**
* A type that represents the union of several other types (a | b | c | ...).
*
* Example:
* ```ts
* const booleanOrNumberType = types.or(types.boolean, types.number)
* ```
*
* @template T Type.
* @param orTypes Possible types.
* @returns
*/
export function typesOr<T extends AnyType[]>(...orTypes: T): T[number]
export function typesOr(
dispatcherOrType: ((sn: any) => AnyType) | AnyType,
...moreOrTypes: AnyType[]
): AnyType {
const orTypes = moreOrTypes.slice()
let finalDispatcher: ((sn: any) => TypeChecker) | undefined
const firstTypeChecker = resolveStandardTypeNoThrow(dispatcherOrType as AnyType)
if (firstTypeChecker) {
orTypes.unshift(firstTypeChecker)
} else {
const dispatcher = dispatcherOrType as (sn: any) => AnyType
finalDispatcher = (sn: any) => {
const type = dispatcher(sn)
const typeChecker = resolveTypeChecker(type)
return typeChecker
}
}
if (orTypes.length <= 0) {
throw failure("or type must have at least 1 possible type")
}
const typeInfoGen: TypeInfoGen = (t) => new OrTypeInfo(t, orTypes.map(resolveStandardType))
return lateTypeChecker(() => {
const checkers = orTypes.map(resolveTypeChecker)
// if the or includes unchecked then it is unchecked
if (checkers.some((tc) => tc.unchecked)) {
return typesUnchecked() as any
}
const getTypeName = (...recursiveTypeCheckers: TypeChecker[]) => {
const typeNames = checkers.map((tc) => {
if (recursiveTypeCheckers.includes(tc)) {
return "..."
}
return tc.getTypeName(...recursiveTypeCheckers, tc)
})
return typeNames.join(" | ")
}
let thisTcBaseType: TypeCheckerBaseType
if (checkers.some((c) => c.baseType !== checkers[0].baseType)) {
thisTcBaseType = TypeCheckerBaseType.Any
} else {
thisTcBaseType = checkers[0].baseType
}
const thisTc: TypeChecker = new TypeChecker(
thisTcBaseType,
(value, path, typeCheckedValue) => {
const someMatchingType = checkers.some((tc) => !tc.check(value, path, typeCheckedValue))
if (someMatchingType) {
return null
} else {
return new TypeCheckError(path, getTypeName(thisTc), value, typeCheckedValue)
}
},
getTypeName,
typeInfoGen,
(value) => {
const valueBaseType = getTypeCheckerBaseTypeFromValue(value)
const checkerForBaseType = checkers.filter(
(c) => c.baseType === valueBaseType || c.baseType === TypeCheckerBaseType.Any
)
if (checkerForBaseType.length === 1 && checkerForBaseType[0].baseType === valueBaseType) {
// when there is only one valid option accept it without asking
// this is done because:
// 1) performance (avoid checking structure if not needed)
// 2) so we can accept untyped models when paired with undefined | null
return checkerForBaseType[0]
}
for (let i = 0; i < checkerForBaseType.length; i++) {
const matchingType = checkerForBaseType[i].snapshotType(value)
if (matchingType) {
return matchingType
}
}
return null
},
(sn) => {
const type = finalDispatcher ? finalDispatcher(sn) : thisTc.snapshotType(sn)
if (!type) {
throw failure(
`snapshot '${JSON.stringify(sn)}' does not match the following type: ${getTypeName(
thisTc
)}`
)
}
return type.fromSnapshotProcessor(sn)
},
(sn) => {
const type = finalDispatcher ? finalDispatcher(sn) : thisTc.snapshotType(sn)
if (!type) {
throw failure(
`snapshot '${JSON.stringify(sn)}' does not match the following type: ${getTypeName(
thisTc
)}`
)
}
return type.toSnapshotProcessor(sn)
}
)
return thisTc
}, typeInfoGen) as any
}
/**
* `types.or` type info.
*/
export class OrTypeInfo extends TypeInfo {
// memoize to always return the same array on the getter
private _orTypeInfos = lazy(() => this.orTypes.map(getTypeInfo))
get orTypeInfos(): ReadonlyArray<TypeInfo> {
return this._orTypeInfos()
}
constructor(
thisType: AnyStandardType,
readonly orTypes: ReadonlyArray<AnyStandardType>
) {
super(thisType)
}
}