Skip to content

Instantly share code, notes, and snippets.

@Dremora
Last active March 29, 2020 20:36
Show Gist options
  • Save Dremora/565fc13e5c28623c5291d8d2f98317f3 to your computer and use it in GitHub Desktop.
Save Dremora/565fc13e5c28623c5291d8d2f98317f3 to your computer and use it in GitHub Desktop.
typescript-json-decoder
import { Decoder, array, decode, lazy, object, oneOf, string } from "./typescript-json-decoder";
type TreeEntry = string | { name: string; contents: TreeRoot };
type TreeRoot = TreeEntry[];
function getTreeEntryDecoder(): Decoder<TreeEntry> {
return oneOf<TreeEntry>(
string,
object({ contents: lazy(getTreeDecoder), name: string })
);
}
function getTreeDecoder(): Decoder<TreeRoot> {
return array(lazy(getTreeEntryDecoder));
}
const treeDecoder = getTreeDecoder();
// This returns a value
decode(treeDecoder, [
"file1",
"file2",
{
contents: [
"file1",
"file2",
{ contents: ["file1", "file2"], name: "folder2" },
],
name: "folder1",
},
]);
// This throws an exception
decode(treeDecoder, [
"file1",
"file2",
42,
{ contents: ["file1", "file2"], name: "folder1" },
]);
export type JSONValue =
| string
| number
| null
| JSONValue[]
| { [key: string]: JSONValue };
class Ok<T> {
constructor(readonly value: T) {}
}
class Err {
constructor(readonly error: string) {}
}
const ok = <T>(value: T) => new Ok(value);
const err = (error: string) => new Err(error);
export type Result<T> = Ok<T> | Err;
export type Decoder<T> = (value: JSONValue) => Result<T>;
export const decode = <T>(decoder: Decoder<T>, value: JSONValue): T => {
const decoded = decoder(value);
if (decoded instanceof Err) {
throw decoded.error;
} else {
return decoded.value;
}
};
export const number = (value: JSONValue): Result<number> => {
if (typeof value === "number") {
return ok(value);
} else {
return err(`${value} is not a number`);
}
};
export const string = (value: JSONValue): Result<string> => {
if (typeof value === "string") {
return ok(value);
} else {
return err(`${value} is not a string`);
}
};
export const optional = <T>(decoder: Decoder<T>) => (
value: JSONValue
): Result<T | undefined> => {
if (typeof value === "undefined" || value === null) {
return ok(undefined);
} else {
return decoder(value);
}
};
export const exact = <T extends JSONValue>(v: T) => (
value: JSONValue
): Result<T> => (v === value ? ok(v) : err(`${value} is not ${v}`));
export const oneOf = <T>(...decoders: Decoder<T>[]) => (
value: JSONValue
): Result<T> => {
for (const decoder of decoders) {
const decoded = decoder(value);
if (decoded instanceof Ok) {
return decoded;
}
}
return err(`Can't decode ${value}`);
};
export const array = <T>(decoder: Decoder<T>) => (
value: JSONValue
): Result<T[]> => {
if (!Array.isArray(value)) {
return err(`${value} is not array`);
}
const decoded: T[] = [];
for (const member of value) {
const decodedMember = decoder(member);
if (decodedMember instanceof Err) {
return decodedMember;
} else {
decoded.push(decodedMember.value);
}
}
return ok(decoded);
};
export const lazy = <T>(getDecoder: () => Decoder<T>) => (
value: JSONValue
): Result<T> => getDecoder()(value);
export type DecoderObject<T> = { [K in keyof Required<T>]: Decoder<T[K]> };
export const dictionary = <T>(decoder: Decoder<T>) => (
value: JSONValue
): Result<{ [key: string]: T }> => {
if (typeof value !== "object" || value === null || Array.isArray(value)) {
return err(`${value} is not an object`);
} else {
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
// @ts-ignore
const decodedValue: { [key: string]: T } = {};
for (const key in value) {
const decoded = decoder(value[key]);
if (decoded instanceof Err) {
return decoded;
} else if (typeof decoded.value !== "undefined") {
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
// @ts-ignore
decodedValue[key] = decoded.value;
}
}
return ok(decodedValue);
}
};
export const object = <
T extends {
[key: string]: unknown;
}
>(
decoders: DecoderObject<T>
) => (value: JSONValue): Result<T> => {
if (typeof value !== "object" || value === null || Array.isArray(value)) {
return err(`${value} is not an object`);
} else {
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
// @ts-ignore
const decodedValue: T = {};
for (const key in decoders) {
const decoded = decoders[key](value[key]);
if (decoded instanceof Err) {
return decoded;
} else if (typeof decoded.value !== "undefined") {
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
// @ts-ignore
decodedValue[key] = decoded.value;
}
}
return ok(decodedValue);
}
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment