UNPKG

mobx-keystone-mindreframer

Version:

A MobX powered state management solution based on data trees with first class support for Typescript, snapshots, patches and much more

149 lines (126 loc) 4.9 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 } from "../modelShared/BaseModelShared" import { modelInfoByClass } from "../modelShared/modelInfo" import { getInternalModelClassPropsInfo } from "../modelShared/modelPropsInfo" import { noDefaultValue } from "../modelShared/prop" import { failure, lateVal } from "../utils" import { getTypeInfo } from "./getTypeInfo" import { resolveTypeChecker } from "./resolveTypeChecker" import type { AnyStandardType, IdentityType } from "./schemas" import { lateTypeChecker, TypeChecker, TypeInfo, TypeInfoGen } from "./TypeChecker" 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) * ``` * * @typeparam M Data model type. * @param modelClass Model class. * @returns */ export function typesDataModelData<M = never, K = M>( modelClass: _ClassOrObject<M, K> ): IdentityType<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` ) } return new TypeChecker( (value, path) => { const resolvedTc = resolveTypeChecker(dataTypeChecker) if (!resolvedTc.unchecked) { return resolvedTc.check(value, path) } return null }, () => typeName, typeInfoGen ) }, typeInfoGen) as any cachedDataModelTypeChecker.set(modelClazz, tc) return tc as any } } /** * `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 = lateVal(() => { 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 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) } }