-
Notifications
You must be signed in to change notification settings - Fork 5
/
index.ts
115 lines (105 loc) 路 3.63 KB
/
index.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
export type Cast<T, Source = unknown> = (data: Source) => T;
export type Infer<Schema extends Cast<unknown>> = ReturnType<Schema>;
// Chainable API
export interface Banditype<T> extends Cast<T> {
map: <E>(extra: Cast<E, T>) => Banditype<E>;
or: <E>(extra: Cast<E>) => Banditype<E | T>;
}
// Core
export const banditype = <T>(cast: Cast<T>): Banditype<T> => {
(cast as Banditype<T>).map = (extra) => banditype((raw) => extra(cast(raw)));
(cast as Banditype<T>).or = (extra) =>
banditype((raw) => {
try {
return cast(raw);
} catch (err) {
return extra(raw);
}
});
return cast as Banditype<T>;
};
// Error helper
export const fail = () => ("bad banditype" as any)() as never;
export const never = () => banditype(() => fail());
export const unknown = () => banditype((raw) => raw);
// literals
// not sure why, but this signature prevents wideing [90] -> number[]
type Primitive = string | number | null | undefined | boolean | symbol | object;
export const enums = <U extends Primitive, T extends readonly U[]>(items: T) =>
banditype((raw) =>
items.includes(raw as T[number]) ? (raw as T[number]) : fail()
);
// Basic types
type Func = (...args: unknown[]) => unknown;
export interface Like {
(tag: string): Banditype<string>;
(tag: number): Banditype<number>;
(tag: boolean): Banditype<boolean>;
(tag: bigint): Banditype<bigint>;
(tag: Func): Banditype<Func>;
(tag: symbol): Banditype<symbol>;
(): Banditype<undefined>;
}
export const like = ((tag: unknown) =>
banditype((raw) => (typeof raw === typeof tag ? raw : fail()))) as Like;
export const string = () => like("");
export const number = () => like(0);
export const boolean = () => like(true);
export const func = () => like(fail);
export const optional = () => like();
export const nullable = () => banditype((raw) => (raw === null ? raw : fail()));
// Classes
export const instance = <T>(proto: new (...args: unknown[]) => T) =>
banditype((raw) => (raw instanceof proto ? (raw as T) : fail()));
// objects
export const record = <Item>(
castValue: Cast<Item>
): Banditype<Record<string, Item>> =>
instance(Object).map((raw: any) => {
const res: Record<string, Item> = {};
for (const key in raw) {
const f = castValue(raw[key]);
f !== undefined && (res[key] = f);
}
return res;
});
export const object = <T = Record<string, never>>(schema: {
[K in keyof T]: Cast<T[K]>;
}) =>
instance(Object).map((raw: any) => {
const res = {} as T;
for (const key in schema) {
const f = schema[key](raw[key]);
f !== undefined && (res[key] = f);
}
return res as T;
});
export const objectLoose = <
T extends Record<string, unknown> = Record<string, never>
>(schema: {
[K in keyof T]: Cast<T[K]>;
}) =>
instance(Object).map((raw: any) => {
const res = { ...raw };
for (const key in schema) {
const f = schema[key](raw[key]);
f !== undefined && (res[key] = f);
}
return res as T;
});
// arrays
export const array = <Item>(castItem: Cast<Item>) =>
instance(Array).map((arr) => arr.map(castItem));
export const tuple = <T extends readonly Cast<unknown>[]>(schema: T) =>
instance(Array).map((arr) => {
return schema.map((cast, i) => cast(arr[i])) as {
-readonly [K in keyof T]: Infer<T[K]>;
};
});
export const set = <T>(castItem: Cast<T>) =>
instance(Set).map((set) => new Set<T>([...set].map(castItem)));
export const map = <K, V>(castKey: Cast<K>, castValue: Cast<V>) =>
instance(Map).map((map) => {
return new Map<K, V>([...map].map(([k, v]) => [castKey(k), castValue(v)]));
});
export const lazy = <T>(cast: () => Cast<T>) => banditype((raw) => cast()(raw));