colinhacks / zod

TypeScript-first schema validation with static type inference
https://zod.dev
MIT License
31.93k stars 1.11k forks source link

[V3] Support template literals #419

Open Armaldio opened 3 years ago

Armaldio commented 3 years ago

Hello :wave:

I recently encountered a case, where my schema returned a string, but a function accept only a TS template string

Here is an example showing the issue: https://codesandbox.io/s/snowy-river-n2ih2?file=/src/index.ts

image

Is there a way, currently or in the future, to be able to do this:

const str = z.template(`${z.number()}.${z.number()}.${z.number()}`)
type Str = z.infer<typeof str>
// => Str: `${number}.${number}.${number}`
colinhacks commented 3 years ago

Good idea! I definitely want to support this eventually.

For now, you'll have to implement this with z.custom:

const literal = z.custom<`${number}.${number}.${number}`>((val) =>
  /^\d+\.\d+\.\d+$/g.test(val as string)
);
tsumo commented 3 years ago

This issue should be renamed to "Support template literals". String literals are already supported in zod.

stale[bot] commented 2 years ago

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

Armaldio commented 2 years ago

No, I still want it StaleBot!

stale[bot] commented 2 years ago

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

Armaldio commented 2 years ago

@colinhacks Any updates on this ?

alexburner commented 2 years ago

I'm here to help in the battle against Stale Bot, I'd be interested in this feature too.

That being said, the z.custom workaround is working perfectly 👍

stale[bot] commented 2 years ago

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

alexburner commented 2 years ago

Stale Bot, no! We still care ❤️

MikeRippon commented 1 year ago

Screw you stalebot!

This would be an amazing feature for formatted string identifiers that contain a type discriminator. Writing/extending custom regex/validation code isn't fun and can be quite error prone. I have a few similar cases where I would love to do something like this:

enum Process {
    BatchJob1 = "BatchJob1",
    BatchJob2 = "BatchJob2",
}

enum PrincipalType {
    User = "User",
    ApiToken = "ApiToken",
    Anonymous = "Anonymous",
    Process = "Process",
}

const Authority = z.string()
type Authority = z.infer<typeof Authority>

const UserId = z.string().uuid()
type UserId = z.infer<typeof UserId>

const ApiTokenId = z.number()
type ApiTokenId = z.infer<typeof ApiTokenId>

// `User:${string}:${string}`
const UserPrincipal = z.template([
    z.literal(PrincipalType.User),
    z.literal(":"),
    Authority,
    z.literal(":"),
    UserId,
])
type UserPrincipal = z.infer<typeof UserPrincipal>

// `ApiToken:${number}`
const ApiTokenPrincipal = z.template([
    z.literal(PrincipalType.ApiToken),
    z.literal(":"),
    ApiTokenId,
])
type ApiTokenPrincipal = z.infer<typeof ApiTokenPrincipal>

// "Anonymous"
const AnonymousPrincipal = z.template([
    z.literal(PrincipalType.Anonymous),
])
type AnonymousPrincipal = z.infer<typeof AnonymousPrincipal>

// "Process:BatchJob1" | "Process:BatchJob2"
const ProcessPrincipal = z.template([
    z.literal(PrincipalType.Process),
    z.literal(":"),
    z.nativeEnum(Process)
])
type ProcessPrincipal = z.infer<typeof ProcessPrincipal>

// `User:${string}:${string}` | `ApiToken:${number}` | "Anonymous" | "Process:BatchJob1" | "Process:BatchJob2"
const Principal = z.union([UserPrincipal, ApiTokenPrincipal, AnonymousPrincipal, ProcessPrincipal])
type Principal = z.infer<typeof Principal>

Enum value expansion looks like it might possibly be hard?

CanRau commented 1 year ago

This would also allow to migrate this TS to zod right?

type SingleBlock = {
    title: string;
    description: string;
    background: boolean;
};

type Block = {
    [T in `block[${number}][${keyof SingleBlock}]`]: T extends `block[${number}][${infer Key extends keyof SingleBlock}]` ? SingleBlock[Key] : never;
};
stale[bot] commented 1 year ago

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

CanRau commented 1 year ago

No staley please don't 😰

olehmisar commented 1 year ago

I tried to implement templateLiteral but did not succeed. Maybe this code will be helpful for those who want to work on this issue. Parsing kinda works. Type inference does not work(infers string)

import { z } from 'zod'

function templateLiteral<S extends readonly string[]>(strings: S, ...schemas: z.ZodSchema[]) {
    if (strings.length != 0 && strings.length !== schemas.length + 1) {
        throw new Error('invalid template literal')
    }

    const len = schemas.length;
    let regexpStr = '^'
    for (let i = 0; i < len; i++) {
        regexpStr += strings[i] + schemaToRegexp(schemas[i]);
    }
    regexpStr += strings[strings.length - 1];
    regexpStr += '$'
    console.log({regexpStr})

    const regexp = new RegExp(regexpStr);
    return z.string().refine(s => regexp.test(s))
}

function schemaToRegexp(schema: z.ZodSchema): string {
    if (schema instanceof z.ZodNumber) {
        return String.raw`(NaN|-?((\d*\.\d+|\d+)([Ee][+-]?\d+)?|Infinity))`
    }
    if (schema instanceof z.ZodString) {
        return '(.*?)'
    }
    if (schema instanceof z.ZodBoolean) {
        return '(true|false)'
    }
    if (schema instanceof z.ZodUndefined) {
        return `(undefined)`
    }
    if (schema instanceof z.ZodNull) {
        return `(null)`
    }
    throw new Error('invalid schema for template string' + JSON.stringify(schema._def))
}

const s1 = templateLiteral`hello ${z.string()}!`
s1.parse('hello BOB!')

const s2 = templateLiteral`hello ${z.string()}! ${z.number()}`
s2.parse('hello BOB! 2.2')
type s2 = z.infer<typeof s2>

templateLiteral`hello ${z.number()}${z.number()}`.parse('hello 1111')

console.log('OK')
maxArturo commented 1 year ago

Looks like we can take off the awaiting-pull-request label @JacobWeisenburger !

stale[bot] commented 1 year ago

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

igalklebanov commented 1 year ago

Oh trust me, it's not stale. 🙃

chihabhajji commented 1 year ago

i have a dream

dugajean commented 10 months ago

This would be awesome to have!

Mugilan-Codes commented 10 months ago

This is actually a superb feature to have. Please implement this.

SerkanSipahi commented 9 months ago

@colinhacks thanks!

ThallesP commented 8 months ago

this would help me a ton because of some libraries that uses template literals as types for hexadecimals.

mkovel commented 7 months ago

@colinhacks thanks Man!

skusez commented 7 months ago

web3 mfers:

const schema = z.object({
    collection: z.literal(`0x${z.string()}`),
})
nifalconi commented 3 months ago

This feature is so important