Last active
April 13, 2022 20:58
-
-
Save alii/30b6aa939e9ca12078cf126196d797d4 to your computer and use it in GitHub Desktop.
Small schema validator inspired by zod. Built so I could learn to some degree how zod works under the hood.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
interface Schema<Out> { | |
parse(value: unknown, path: string): Out; | |
} | |
type Resolve<T> = T extends Schema<infer R> ? Resolve<R> : T; | |
function bool(): Schema<boolean> { | |
return { | |
parse(value, path) { | |
const valid = typeof value === "boolean"; | |
if (!valid) { | |
throw new Error(`Expected boolean at \`${path}\`, \`found ${value}\``); | |
} | |
return value; | |
}, | |
}; | |
} | |
function str(): Schema<string> { | |
return { | |
parse(value, path) { | |
const valid = typeof value === "string"; | |
if (!valid) { | |
throw new Error(`Expected string at \`${path}\`, found \`${value}\``); | |
} | |
return value; | |
}, | |
}; | |
} | |
function lit<T>(v: T): Schema<T> { | |
return { | |
parse(value, path) { | |
if (value !== v) { | |
throw new Error(`Expected ${v} (${typeof v}) at \`${path}\`, found \`${value}\``); | |
} | |
return value as T; | |
}, | |
}; | |
} | |
function num(): Schema<number> { | |
return { | |
parse(value, path) { | |
const valid = typeof value === "number"; | |
if (!valid) { | |
throw new Error(`Expected number at \`${path}\`, found \`${value}\``); | |
} | |
return value; | |
}, | |
}; | |
} | |
function obj<T extends Record<string, Schema<unknown>>>(shape: T): Schema<{ [K in keyof T]: Resolve<T[K]> }> { | |
return { | |
parse(value, path) { | |
if (!value || typeof value !== "object") { | |
throw new Error(`Expected object at \`${path}\`, found \`${value}\``); | |
} | |
const result: Partial<{ [K in keyof T]: Resolve<T[K]> }> = {}; | |
for (const key in shape) { | |
const schema = shape[key]; | |
result[key] = schema.parse(value[key as keyof typeof value], `${path}.${key}`) as Resolve< | |
T[Extract<keyof T, string>] | |
>; | |
} | |
return result as { [K in keyof T]: Resolve<T[K]> }; | |
}, | |
}; | |
} | |
function parse<T>(schema: Schema<T>, value: unknown): T { | |
return schema.parse(value, "<root>"); | |
} | |
function safeParse<T>( | |
schema: Schema<T>, | |
value: unknown | |
): | |
| { | |
success: true; | |
data: T; | |
} | |
| { | |
success: false; | |
error: Error; | |
} { | |
try { | |
const data = parse(schema, value); | |
return { | |
success: true as const, | |
data, | |
}; | |
} catch (e: unknown) { | |
return { | |
success: false as const, | |
error: e as Error, | |
}; | |
} | |
} | |
const mySchema = obj({ | |
bruh: bool(), | |
another: lit("bruh" as const), | |
details: obj({ | |
age: num(), | |
deep: obj({ | |
something: num(), | |
deeeeeper: obj({ | |
prop: bool(), | |
}), | |
}), | |
}), | |
}); | |
const result = parse(mySchema, { | |
bruh: true, | |
another: "bruh", | |
details: { | |
age: 2, | |
deep: { | |
something: 2, | |
deeeeeper: { | |
prop: true, | |
}, | |
}, | |
}, | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
deeeeeper