diff --git a/src/index.ts b/src/index.ts index 1a4f581..6359c6b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,8 +1,8 @@ import type { StandardSchemaV1 } from '@standard-schema/spec' -import type { Issues } from './types' +import type { AnyRecord, Issues } from './types' import { reactive, watch, toValue, type MaybeRefOrGetter } from 'vue' -export function useValidation>(data: MaybeRefOrGetter, schema: StandardSchemaV1) { +export function useValidation(data: MaybeRefOrGetter, schema: StandardSchemaV1) { const issues = reactive>({}) const clearIssues = () => Object.keys(issues).forEach((key) => delete issues[key]) diff --git a/src/types.ts b/src/types.ts index 0198dfc..f883bea 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,7 +1,15 @@ -type Combine = T extends unknown - ? T & Partial, never>> - : never +/** Generate a union of all property keys from T, including keys from all members when T is a union itself. */ +type KeysOf = T extends unknown ? keyof T : never +/** Pick property type for a possibly missing key */ +type PropType = T extends { [P in K]?: unknown } ? T[K] : never + +/** Nested record of issue message arrays */ export type Issues = { - [Key in keyof Combine]?: Combine[Key] extends object ? Issues[Key]> : string[] + [K in KeysOf]?: PropType extends object + ? Issues> // if property type is object (or array): recurse + : string[] // else: array of messages } + +/** Any record with unknown value type */ +export type AnyRecord = Record diff --git a/tests/index.test.ts b/tests/index.test.ts index c9bcbff..3ae5ee8 100644 --- a/tests/index.test.ts +++ b/tests/index.test.ts @@ -392,6 +392,44 @@ describe('useValidation', () => { expect(issues.address?.street).toBeDefined() expect(issues.address?.city).toBeDefined() }) + + it('should accept complex union typed data', async () => { + type Auth = + | { mode: 'login'; auth: { password: string; email: { address: string } } } + | { mode: 'signup'; auth: { code: string; provider: { id: string } } } + + const loginSchema = z.object({ + mode: z.literal('login'), + auth: z.object({ + password: z.string().min(1, 'Password is required'), + email: z.object({ + address: z.email('Invalid email address'), + }), + }), + }) + + const signupSchema = z.object({ + mode: z.literal('signup'), + auth: z.object({ + code: z.string().length(6, 'Code is required'), + provider: z.object({ + id: z.enum(['basic', 'oauth'], 'Invalid provider'), + }), + }), + }) + + const schema = z.discriminatedUnion('mode', [loginSchema, signupSchema]) + + const data = reactive({ mode: 'login', auth: { password: '', email: { address: '' } } }) + + const { validate, issues } = useValidation(data, schema) + await validate() + + expect(issues.auth?.password).toBeDefined() + expect(issues.auth?.email?.address).toBeDefined() + expect(issues.auth?.code).not.toBeDefined() + expect(issues.auth?.provider?.id).not.toBeDefined() + }) }) describe('clearIssues function', () => {