UNPKG

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 (160 loc) 5.77 kB
import type { O } from "ts-toolbelt" import type { AnyDataModel } from "../../dataModel/BaseDataModel" import { getDataModelMetadata } from "../../dataModel/getDataModelMetadata" import { isDataModelClass } from "../../dataModel/utils" import type { ModelClass, ModelData, ModelUntransformedData, } from "../../modelShared/BaseModelShared" import { modelInfoByClass } from "../../modelShared/modelInfo" import { getInternalModelClassPropsInfo } from "../../modelShared/modelPropsInfo" import { noDefaultValue } from "../../modelShared/prop" import { failure, lazy } from "../../utils" import { TypeChecker, TypeCheckerBaseType, TypeInfo, TypeInfoGen, lateTypeChecker, } from "../TypeChecker" import { getTypeInfo } from "../getTypeInfo" import { registerStandardTypeResolver, resolveTypeChecker } from "../resolveTypeChecker" import type { AnyStandardType, IdentityType } from "../schemas" const cachedDataModelTypeChecker = new WeakMap<ModelClass<AnyDataModel>, TypeChecker>() type _Class<T> = abstract new (...args: any[]) => T type _ClassOrObject<M, K> = K extends M ? object : _Class<K> | (() => _Class<K>) /** * A type that represents a data model data. * The type referenced in the model decorator will be used for type checking. * * Example: * ```ts * const someDataModelDataType = types.dataModelData(SomeModel) * // or for recursive models * const someDataModelDataType = types.dataModelData<SomeModel>(() => SomeModel) * ``` * * @template M Data model type. * @param modelClass Model class. * @returns */ export function typesDataModelData<M = never, K = M>( modelClass: _ClassOrObject<M, K> ): IdentityType< | ModelUntransformedData< K extends M ? (M extends AnyDataModel ? M : never) : K extends AnyDataModel ? K : never > | ModelData< K extends M ? (M extends AnyDataModel ? M : never) : K extends AnyDataModel ? K : never > > { // if we type it any stronger then recursive defs and so on stop working if (!isDataModelClass(modelClass) && typeof modelClass === "function") { // resolve later const modelClassFn = modelClass as () => ModelClass<AnyDataModel> const typeInfoGen: TypeInfoGen = (t) => new DataModelDataTypeInfo(t, modelClassFn()) return lateTypeChecker(() => typesDataModelData(modelClassFn()) as any, typeInfoGen) as any } else { const modelClazz: ModelClass<AnyDataModel> = modelClass as any const cachedTypeChecker = cachedDataModelTypeChecker.get(modelClazz) if (cachedTypeChecker) { return cachedTypeChecker as any } const typeInfoGen: TypeInfoGen = (t) => new DataModelDataTypeInfo(t, modelClazz) const tc = lateTypeChecker(() => { const modelInfo = modelInfoByClass.get(modelClazz)! const typeName = `DataModelData(${modelInfo.name})` const dataTypeChecker = getDataModelMetadata(modelClazz).dataType if (!dataTypeChecker) { throw failure( `type checking cannot be performed over data model data of type '${modelInfo.name}' since that model type has no data type declared, consider adding a data type or using types.unchecked() instead` ) } const resolvedDataTypeChecker = resolveTypeChecker(dataTypeChecker) const thisTc: TypeChecker = new TypeChecker( TypeCheckerBaseType.Object, (value, path, typeCheckedValue) => { return resolvedDataTypeChecker.check(value, path, typeCheckedValue) }, () => typeName, typeInfoGen, (value) => { return resolvedDataTypeChecker.snapshotType(value) ? thisTc : null }, (sn: Record<string, unknown>) => { return resolvedDataTypeChecker.fromSnapshotProcessor(sn) }, (sn: Record<string, unknown>) => { return resolvedDataTypeChecker.toSnapshotProcessor(sn) } ) return thisTc }, typeInfoGen) as any cachedDataModelTypeChecker.set(modelClazz, tc) return tc } } /** * `types.dataModelData` type info for a model props. */ export interface DataModelDataTypeInfoProps { readonly [propName: string]: Readonly<{ type: AnyStandardType | undefined typeInfo: TypeInfo | undefined hasDefault: boolean default: any }> } /** * `types.dataModelData` type info. */ export class DataModelDataTypeInfo extends TypeInfo { private _props = lazy(() => { const objSchema = getInternalModelClassPropsInfo(this.modelClass) const propTypes: O.Writable<DataModelDataTypeInfoProps> = {} Object.keys(objSchema).forEach((propName) => { const propData = objSchema[propName] const type = propData._typeChecker as any as AnyStandardType | undefined let typeInfo: TypeInfo | undefined if (type) { typeInfo = getTypeInfo(type) } let hasDefault = false let defaultValue: any if (propData._defaultFn !== noDefaultValue) { defaultValue = propData._defaultFn hasDefault = true } else if (propData._defaultValue !== noDefaultValue) { defaultValue = propData._defaultValue hasDefault = true } propTypes[propName] = { type, typeInfo, hasDefault, default: defaultValue, } }) return propTypes }) get props(): DataModelDataTypeInfoProps { return this._props() } get modelType(): string { const modelInfo = modelInfoByClass.get(this.modelClass)! return modelInfo.name } constructor( thisType: AnyStandardType, readonly modelClass: ModelClass<AnyDataModel> ) { super(thisType) } } /** * @internal */ export function registerDataModelDataStandardTypeResolver() { registerStandardTypeResolver((v) => (isDataModelClass(v) ? typesDataModelData(v) : undefined)) }