From 70b4ff8a1f32f2e21f2ee9f5fc9d2aec75a96145 Mon Sep 17 00:00:00 2001 From: Kyedo <31018128+itskyedo@users.noreply.github.com> Date: Wed, 21 Jan 2026 11:16:11 -0800 Subject: [PATCH] Add InstanceCheck for class validation This creates a new check to enable validation for class constructors. --- .changeset/goofy-dogs-knock.md | 5 ++ packages/typestone/src/checks/instance/def.ts | 53 +++++++++++++++++++ .../typestone/src/checks/instance/index.ts | 2 + .../typestone/src/checks/instance/instance.ts | 17 ++++++ .../typestone/src/checks/instance/types.ts | 1 + .../typestone/src/checks/instance/utils.ts | 17 ++++++ packages/typestone/src/index.ts | 1 + 7 files changed, 96 insertions(+) create mode 100644 .changeset/goofy-dogs-knock.md create mode 100644 packages/typestone/src/checks/instance/def.ts create mode 100644 packages/typestone/src/checks/instance/index.ts create mode 100644 packages/typestone/src/checks/instance/instance.ts create mode 100644 packages/typestone/src/checks/instance/types.ts create mode 100644 packages/typestone/src/checks/instance/utils.ts diff --git a/.changeset/goofy-dogs-knock.md b/.changeset/goofy-dogs-knock.md new file mode 100644 index 0000000..5c3ac8f --- /dev/null +++ b/.changeset/goofy-dogs-knock.md @@ -0,0 +1,5 @@ +--- +'typestone': minor +--- + +Adds InstanceCheck to enable validating class instances diff --git a/packages/typestone/src/checks/instance/def.ts b/packages/typestone/src/checks/instance/def.ts new file mode 100644 index 0000000..bda9b70 --- /dev/null +++ b/packages/typestone/src/checks/instance/def.ts @@ -0,0 +1,53 @@ +import { + type ErrorHandlerParameter, + type ErrorMap, + errorParamToErrorMap, +} from '../../error/error.ts'; +import { processIssue } from '../../internal/process/process-issue.ts'; +import { type CheckDef } from '../check/check.ts'; +import { type InstanceCheck } from './instance.ts'; +import { type AnyClass } from './types.ts'; +import { getConstructorName } from './utils.ts'; + +export type InstanceErrorMap = ErrorMap<'invalid_type'>; + +export interface InstanceDef extends CheckDef< + InstanceType, + InstanceType, + InstanceErrorMap +> { + readonly type: 'instance'; + + readonly ctor: TClass; +} + +export function instanceDef( + ctor: TClass, + error: ErrorHandlerParameter, +): InstanceDef { + return { + kind: 'check', + type: 'instance', + errorMap: errorParamToErrorMap(error), + + ctor, + + _process, + }; +} + +const _process: InstanceCheck['_process'] = function* (context) { + const ctorName = getConstructorName(this.ctor) ?? 'No constructor name'; + const valueCtorName = + getConstructorName(context.value) ?? 'No constructor name'; + if (!((context.value as unknown) instanceof this.ctor)) { + yield* processIssue(this.errorMap, { + code: 'invalid_type', + path: context.path, + input: context.value, + expected: ctorName, + received: valueCtorName, + message: `Expected an instance of ${ctorName}.`, + }); + } +}; diff --git a/packages/typestone/src/checks/instance/index.ts b/packages/typestone/src/checks/instance/index.ts new file mode 100644 index 0000000..b86661c --- /dev/null +++ b/packages/typestone/src/checks/instance/index.ts @@ -0,0 +1,2 @@ +export * from './instance.ts'; +export * from './types.ts'; diff --git a/packages/typestone/src/checks/instance/instance.ts b/packages/typestone/src/checks/instance/instance.ts new file mode 100644 index 0000000..744c281 --- /dev/null +++ b/packages/typestone/src/checks/instance/instance.ts @@ -0,0 +1,17 @@ +import { type ErrorHandlerParameter } from '../../error/error.ts'; +import { createCheck, type DefineCheck } from '../check/check.ts'; +import { type InstanceDef, instanceDef, type InstanceErrorMap } from './def.ts'; +import { type AnyClass } from './types.ts'; + +export interface InstanceCheck< + TClass extends AnyClass = AnyClass, +> extends DefineCheck> { + kind: 'check'; +} + +export function instance( + ctor: TClass, + error?: ErrorHandlerParameter, +): InstanceCheck { + return createCheck(instanceDef(ctor, error ?? {})); +} diff --git a/packages/typestone/src/checks/instance/types.ts b/packages/typestone/src/checks/instance/types.ts new file mode 100644 index 0000000..f64d451 --- /dev/null +++ b/packages/typestone/src/checks/instance/types.ts @@ -0,0 +1 @@ +export type AnyClass = new (...args: any[]) => any; diff --git a/packages/typestone/src/checks/instance/utils.ts b/packages/typestone/src/checks/instance/utils.ts new file mode 100644 index 0000000..580c56e --- /dev/null +++ b/packages/typestone/src/checks/instance/utils.ts @@ -0,0 +1,17 @@ +export function getConstructorName(value: unknown): string | null { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const isConstructor = typeof value === 'function' && value.prototype; + if (isConstructor) { + return value.name || null; + } + + const isObject = typeof value === 'object' && value !== null; + if (isObject) { + const ctor = value.constructor; + if (typeof ctor === 'function') { + return ctor.name || null; + } + } + + return null; +} diff --git a/packages/typestone/src/index.ts b/packages/typestone/src/index.ts index 8aaedf1..ba5fae5 100644 --- a/packages/typestone/src/index.ts +++ b/packages/typestone/src/index.ts @@ -7,6 +7,7 @@ export * from './checks/ends-with/index.ts'; export * from './checks/eq/index.ts'; export * from './checks/gt/index.ts'; export * from './checks/gte/index.ts'; +export * from './checks/instance/index.ts'; export * from './checks/integer/index.ts'; export * from './checks/lt/index.ts'; export * from './checks/lte/index.ts';