zod icon indicating copy to clipboard operation
zod copied to clipboard

[V3] Support template literals

Open Armaldio opened this issue 3 years ago • 25 comments

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}`

Armaldio avatar Apr 30 '21 15:04 Armaldio

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)
);

colinhacks avatar May 02 '21 02:05 colinhacks

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

tsumo avatar Jun 03 '21 13:06 tsumo

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.

stale[bot] avatar Mar 02 '22 04:03 stale[bot]

No, I still want it StaleBot!

Armaldio avatar Mar 02 '22 07:03 Armaldio

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.

stale[bot] avatar May 01 '22 14:05 stale[bot]

@colinhacks Any updates on this ?

Armaldio avatar May 02 '22 06:05 Armaldio

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 👍

alexburner avatar May 05 '22 16:05 alexburner

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.

stale[bot] avatar Jul 04 '22 17:07 stale[bot]

Stale Bot, no! We still care ❤️

alexburner avatar Jul 04 '22 18:07 alexburner

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?

MikeRippon avatar Jul 30 '22 17:07 MikeRippon

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;
};

CanRau avatar Sep 15 '22 16:09 CanRau

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.

stale[bot] avatar Nov 14 '22 20:11 stale[bot]

No staley please don't 😰

CanRau avatar Nov 14 '22 22:11 CanRau

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')

olehmisar avatar Dec 09 '22 13:12 olehmisar

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

maxArturo avatar Jan 19 '23 19:01 maxArturo

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.

stale[bot] avatar Apr 19 '23 22:04 stale[bot]

Oh trust me, it's not stale. 🙃

igalklebanov avatar Apr 20 '23 23:04 igalklebanov

i have a dream

chihabhajji avatar Jun 18 '23 08:06 chihabhajji

This would be awesome to have!

dugajean avatar Sep 09 '23 16:09 dugajean

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

Mugilan-Codes avatar Sep 19 '23 16:09 Mugilan-Codes

@colinhacks thanks!

SerkanSipahi avatar Oct 01 '23 20:10 SerkanSipahi

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

ThallesP avatar Nov 13 '23 17:11 ThallesP

@colinhacks thanks Man!

mkovel avatar Nov 22 '23 23:11 mkovel

web3 mfers:

const schema = z.object({
    collection: z.literal(`0x${z.string()}`),
})

skusez avatar Nov 27 '23 07:11 skusez

This feature is so important

nifalconi avatar Mar 28 '24 14:03 nifalconi