Created
May 22, 2023 13:31
-
-
Save mizchi/2a362b498d806cdf1ed0840f5b59d411 to your computer and use it in GitHub Desktop.
This file contains 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
// lightweight zod | |
type Validator<Expect> = (input: any) => input is Expect; | |
type ValidatorObject<Expect extends {}> = (input: any) => input is { | |
[K in keyof Expect]: Expect[K] extends Validator<infer CT> ? CT : never; | |
}; | |
type ValidatorsToUnion<Vs> = Vs extends Array<Validator<infer T>> ? T | |
: never; | |
type EnumsToUnion<Vs> = Vs extends Array<Validator<infer T>> ? T | |
: never; | |
type Infer<T> = T extends ValidatorObject<any> ? { | |
[K in keyof T]: Infer<T[K]>; | |
} | |
: T extends Validator<infer E> ? E | |
: never; | |
const $const = | |
<T extends number | string | boolean | undefined | void | symbol>( | |
def: T, | |
): Validator<T> => | |
(input: any): input is T => { | |
return input === def; | |
}; | |
const $undefined: Validator<undefined> = (input: any): input is undefined => { | |
return input === undefined; | |
}; | |
const $null: Validator<null> = (input: any): input is null => { | |
return input === null; | |
}; | |
const $void: Validator<void> = (input: any): input is void => { | |
return input != null; | |
}; | |
const $opt = | |
<T>(validator: Validator<T>): Validator<T | void> => | |
(input: any): input is T | void => { | |
return input == null || validator(input); | |
}; | |
const $string: Validator<string> = (input: any): input is string => { | |
return typeof input === "string"; | |
}; | |
const $symbol: Validator<symbol> = (input: any): input is symbol => { | |
return typeof input === "symbol"; | |
}; | |
const $number: Validator<number> = (input: any): input is number => { | |
return typeof input === "number"; | |
}; | |
const $boolean: Validator<boolean> = (input: any): input is boolean => { | |
return typeof input === "boolean"; | |
}; | |
const $enum = | |
<E extends Array<string>>(enums: E): Validator<E[number]> => | |
(input: any): input is E[number] => { | |
return enums.includes(input); | |
}; | |
// https://stackoverflow.com/questions/75405517/typescript-generic-function-where-parameters-compose-into-the-return-type | |
type TupleToIntersection<T extends any[]> = { | |
[I in keyof T]: (x: T[I]) => void; | |
}[number] extends (x: infer R) => void ? R : never; | |
const $intersection = <T extends any[]>( | |
validators: [...{ [I in keyof T]: Validator<T[I]> }], | |
): Validator<TupleToIntersection<T>> => { | |
return ((doc: any): doc is TupleToIntersection<T> => { | |
for (const validator of validators) { | |
if (!validator(doc)) return false; | |
} | |
return true; | |
}); | |
}; | |
const $union = | |
<T, Vs extends Array<Validator<T>>>(validators: Vs) => | |
(input: any): input is ValidatorsToUnion<Vs> => { | |
for (const validator of validators) { | |
if (validator(input)) { | |
return true; | |
} | |
} | |
return true; | |
}; | |
const $object = < | |
Map extends { | |
[key: string]: Validator<any>; | |
}, | |
>(vmap: Map) => | |
( | |
input: any, | |
): input is { | |
[K in keyof Map]: Map[K] extends ValidatorObject<infer O> ? O | |
: Map[K] extends Validator<infer I> ? I | |
: never; | |
} => { | |
for (const [key, validator] of Object.entries(vmap)) { | |
if (!validator(input?.[key])) { | |
return false; | |
} | |
} | |
return true; | |
}; | |
const $array = < | |
T extends Validator<any>, | |
>(child: T) => | |
(input: any): input is Array< | |
T extends Validator<infer O> ? O : never | |
> => { | |
return Array.isArray(input) && input.every((i) => { | |
return child(i); | |
}); | |
}; | |
// ---------- | |
// How to use | |
const validate = $object({ | |
name: $string, | |
age: $number, | |
familyName: $opt($string), | |
abc: $enum(["a" as const, "b" as const, "c" as const]), | |
nested: $object({ | |
age: $number, | |
}), | |
static: $const("static"), | |
items: $array($object({ | |
a: $string, | |
b: $boolean, | |
})), | |
complex: $array($union([ | |
$object({ a: $string }), | |
$object({ b: $number }), | |
])), | |
sec: $intersection([$string, $const("x")]), | |
}); | |
const v: Infer<typeof validate> = { | |
name: "aaa", | |
age: 1, | |
familyName: null, | |
abc: "b", | |
nested: { | |
age: 1, | |
}, | |
static: "static", | |
items: [ | |
{ | |
a: "", | |
b: true, | |
}, | |
{ | |
a: "", | |
b: false, | |
}, | |
], | |
complex: [ | |
{ a: "" }, | |
{ b: 1 }, | |
], | |
sec: "x", | |
}; | |
if (validate(v)) { | |
const _: string = v.name; | |
const __: number = v.age; | |
const ___: string | void = v.familyName; | |
const ____: "a" | "b" | "c" = v.abc; | |
const _____: { age: number } = v.nested; | |
const ______: "static" = v.static; | |
const _______: Array<{ | |
a: string; | |
b: boolean; | |
}> = v.items; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment