Z
Zod3mo ago
Steve

Steve - I have this unique case where I want to...

I have this unique case where I want to filter out elements that do not match my union. Is there any way to do that using zod? Basically: z.union([ ... ]).strip()
Solution:
```ts export const TipTapDocumentDto = z .object({ type: z.literal("doc"), attrs: z.object({...
Jump to solution
13 Replies
Steve
SteveOP3mo ago
Currently I am thinking of using z.preprocess, would this be the way to go?
Scott Trinh
Scott Trinh3mo ago
What do you mean "filter out"? Are you working with an array of this union?
Steve
SteveOP3mo ago
Oh yeah, sorry, correct.
export const TipTapDocumentDto = z
.object({
type: z.literal('doc'),
attrs: z.object({
identifier: z.string(),
}),
content: z.array(z.union([TipTapParagraphDto, TipTapHeadingDto])).default([]),
})
.strict()
export type TipTapDocumentDto = z.infer<typeof TipTapDocumentDto>
export const TipTapDocumentDto = z
.object({
type: z.literal('doc'),
attrs: z.object({
identifier: z.string(),
}),
content: z.array(z.union([TipTapParagraphDto, TipTapHeadingDto])).default([]),
})
.strict()
export type TipTapDocumentDto = z.infer<typeof TipTapDocumentDto>
In the content property, I have an array of a union of objects, also having a type property. And for now I just don't want to bother with anything that does not have type being equal to 'heading' or 'paragraph' for context:
Scott Trinh
Scott Trinh3mo ago
I would probably use catch and transform with a filter
Steve
SteveOP3mo ago
import { z } from 'zod'

const SimpleMarkDto = <T extends string>(typeName: T) =>
z.object({
type: z.literal(typeName),
})

const IdentifierMarkDto = z
.object({
type: z.literal('identifier'),
attrs: z.object({
identifier: z.string(),
}),
})
.strict()

const BoldMarkDto = SimpleMarkDto('bold')
const ItalicMarkDto = SimpleMarkDto('italic')
const StrikeMarkDto = SimpleMarkDto('strike')
const CodeMarkDto = SimpleMarkDto('code')
const SubscriptMarkDto = SimpleMarkDto('subscript')
const SuperscriptMarkDto = SimpleMarkDto('superscript')
const UnderlineMarkDto = SimpleMarkDto('underline')

export const TipTapMarkDto = z.union([
SuperscriptMarkDto,
SubscriptMarkDto,
CodeMarkDto,
StrikeMarkDto,
BoldMarkDto,
ItalicMarkDto,
UnderlineMarkDto,
IdentifierMarkDto,
])
export type TipTapMarkDto = z.infer<typeof TipTapMarkDto>

export const TipTapTextDto = z
.object({
type: z.literal('text'),
text: z.string(),
marks: z.array(TipTapMarkDto).default([]),
})
.strict()
.refine((dto) => dto.marks.some((mark) => mark.type === 'identifier'), {
message: 'Should have an identifier',
})
export type TipTapTextDto = z.infer<typeof TipTapTextDto>

const content = z.array(TipTapTextDto).default([])

export const TipTapHeadingDto = z
.object({
type: z.literal('heading'),
attrs: z.object({
identifier: z.string(),
level: z.number(),
}),
content,
})
.strict()
export type TipTapHeadingDto = z.infer<typeof TipTapHeadingDto>

export const TipTapParagraphDto = z
.object({
type: z.literal('paragraph'),
attrs: z.object({
identifier: z.string(),
}),
content,
})
.strict()
export type TipTapParagraphDto = z.infer<typeof TipTapParagraphDto>
import { z } from 'zod'

const SimpleMarkDto = <T extends string>(typeName: T) =>
z.object({
type: z.literal(typeName),
})

const IdentifierMarkDto = z
.object({
type: z.literal('identifier'),
attrs: z.object({
identifier: z.string(),
}),
})
.strict()

const BoldMarkDto = SimpleMarkDto('bold')
const ItalicMarkDto = SimpleMarkDto('italic')
const StrikeMarkDto = SimpleMarkDto('strike')
const CodeMarkDto = SimpleMarkDto('code')
const SubscriptMarkDto = SimpleMarkDto('subscript')
const SuperscriptMarkDto = SimpleMarkDto('superscript')
const UnderlineMarkDto = SimpleMarkDto('underline')

export const TipTapMarkDto = z.union([
SuperscriptMarkDto,
SubscriptMarkDto,
CodeMarkDto,
StrikeMarkDto,
BoldMarkDto,
ItalicMarkDto,
UnderlineMarkDto,
IdentifierMarkDto,
])
export type TipTapMarkDto = z.infer<typeof TipTapMarkDto>

export const TipTapTextDto = z
.object({
type: z.literal('text'),
text: z.string(),
marks: z.array(TipTapMarkDto).default([]),
})
.strict()
.refine((dto) => dto.marks.some((mark) => mark.type === 'identifier'), {
message: 'Should have an identifier',
})
export type TipTapTextDto = z.infer<typeof TipTapTextDto>

const content = z.array(TipTapTextDto).default([])

export const TipTapHeadingDto = z
.object({
type: z.literal('heading'),
attrs: z.object({
identifier: z.string(),
level: z.number(),
}),
content,
})
.strict()
export type TipTapHeadingDto = z.infer<typeof TipTapHeadingDto>

export const TipTapParagraphDto = z
.object({
type: z.literal('paragraph'),
attrs: z.object({
identifier: z.string(),
}),
content,
})
.strict()
export type TipTapParagraphDto = z.infer<typeof TipTapParagraphDto>
Steve
SteveOP3mo ago
I would probably use catch and transform with a filter
Transform would be executed on the output, right? I want to filter it out of the input for now .catch would be possible for now.
Scott Trinh
Scott Trinh3mo ago
Lemme demonstrate, maybe we're saying the same thing
Solution
Scott Trinh
Scott Trinh3mo ago
export const TipTapDocumentDto = z
.object({
type: z.literal("doc"),
attrs: z.object({
identifier: z.string(),
}),
content: z
.array(
z.union([TipTapParagraphDto, TipTapHeadingDto, z.null()]).catch(null),
)
.default([])
.transform((xs) =>
xs.filter((x): x is TipTapParagraphDto | TipTapHeadingDto =>
Boolean(x),
),
),
})
.strict();
export const TipTapDocumentDto = z
.object({
type: z.literal("doc"),
attrs: z.object({
identifier: z.string(),
}),
content: z
.array(
z.union([TipTapParagraphDto, TipTapHeadingDto, z.null()]).catch(null),
)
.default([])
.transform((xs) =>
xs.filter((x): x is TipTapParagraphDto | TipTapHeadingDto =>
Boolean(x),
),
),
})
.strict();
Steve
SteveOP3mo ago
Would it modify my output type to also include null or not? I am afraid it would, let me test This works brilliantly, thanks @Scott Trinh !
type DocumentContentMember = TipTapParagraphDto | TipTapHeadingDto

export const TipTapDocumentDto = z
.object({
type: z.literal('doc'),
attrs: z.object({
identifier: z.string(),
}),
/**
* @TODO remove logic for filtering out unsupported element types
*/
content: z
.array(z.union([TipTapParagraphDto, TipTapHeadingDto]).nullable().catch(null))
.default([])
.transform((xs: (DocumentContentMember | null)[]): DocumentContentMember[] => {
return xs.filter((x): x is DocumentContentMember => x !== null)
}),
})
.strict()
export type TipTapDocumentDto = z.infer<typeof TipTapDocumentDto>
type DocumentContentMember = TipTapParagraphDto | TipTapHeadingDto

export const TipTapDocumentDto = z
.object({
type: z.literal('doc'),
attrs: z.object({
identifier: z.string(),
}),
/**
* @TODO remove logic for filtering out unsupported element types
*/
content: z
.array(z.union([TipTapParagraphDto, TipTapHeadingDto]).nullable().catch(null))
.default([])
.transform((xs: (DocumentContentMember | null)[]): DocumentContentMember[] => {
return xs.filter((x): x is DocumentContentMember => x !== null)
}),
})
.strict()
export type TipTapDocumentDto = z.infer<typeof TipTapDocumentDto>
Scott Trinh
Scott Trinh3mo ago
yeah, the transform will correctly remove the null type due to the type guard filter.
Steve
SteveOP3mo ago
You're a genius
Scott Trinh
Scott Trinh3mo ago
🙇‍♂️ If you ever need to hand null as a valid member, the same thing works if you use a custom sentinel value instead of null.
Steve
SteveOP3mo ago
sweet
Want results from more Discord servers?
Add your server