Skip to content

Instantly share code, notes, and snippets.

@JSuder-xx
Created February 8, 2021 17:06
Show Gist options
  • Save JSuder-xx/8d27ca5a70960daeb2cb18cec709ad80 to your computer and use it in GitHub Desktop.
Save JSuder-xx/8d27ca5a70960daeb2cb18cec709ad80 to your computer and use it in GitHub Desktop.
A micro JSON decoder API in TypeScript.
/** Micro Api for building decoders. */
module DecoderApi {
type Unknown = unknown;
type ErrorMessage = { kind: "error"; error: string }
export const errorMessage = (error: string): ErrorMessage => ({ kind: "error", error });
export type DecodeResult<value> = value | ErrorMessage
export const isErrorMessage = <value extends unknown>(result: DecodeResult<value>): result is ErrorMessage =>
!!result &&
typeof result === "object" &&
(result as any).kind === "error";
export const isSuccess = <value extends unknown>(result: DecodeResult<value>): result is value => !isErrorMessage(result);
export type Decoder<refined> = (val: Unknown) => DecodeResult<refined>;
export type TypeOfDecoder<refiner> = refiner extends Decoder<infer refined> ? refined : never;
export type DecoderMap = { [propertyName: string]: Decoder<any> }
export type ObjectFromDecoderMap<map extends { [propertyName: string]: Decoder<any> }> = {
[propertyName in keyof map]: TypeOfDecoder<map[propertyName]>;
}
export const decodeString: Decoder<string> = (val: Unknown) => typeof val === "string" ? val : errorMessage(`Expected a string but got ${typeof val}`);
export const decodeNumber: Decoder<number> = (val: Unknown) => typeof val === "number" ? val : errorMessage(`Expected a number but got ${typeof val}`);
export const decodeBoolean: Decoder<boolean> = (val: Unknown) => typeof val === "boolean" ? val : errorMessage(`Expected a boolean but got ${typeof val}`);
export const decodeArray: Decoder<any[]> = (val: Unknown) => !!val && typeof (val as any).push === "function" ? (val as any[]) : errorMessage(`Expected an array`);
/** Create a predicate which tests for an object. */
export const createObjectDecoder = <decoderMap extends DecoderMap>(decoderMap: decoderMap): Decoder<ObjectFromDecoderMap<decoderMap>> =>
(obj: any) => {
if (obj === null) return errorMessage(`Expected object; got null.`);
if (typeof obj !== "object") return errorMessage(`Expected object; got ${typeof obj}`);
for(const propertyName in decoderMap) {
const decoder = decoderMap[propertyName];
const result = decoder(obj[propertyName]);
if (isErrorMessage(result))
return errorMessage(`For property '${propertyName}': ${result.error}`);
}
return obj;
}
export const createArrayDecoder = <Item extends Unknown>(itemDecoder: Decoder<Item>): Decoder<Item[]> =>
(val: any) => {
const arrayResult = decodeArray(val);
if (isErrorMessage(arrayResult)) return arrayResult;
for(let idx = 0; idx < arrayResult.length; idx++) {
const result = itemDecoder(arrayResult[idx]);
if (isErrorMessage(result))
return errorMessage(`Array index ${idx}: ${result.error}`);
}
return val;
}
/** If you trust it will be a homogenous array then use this for performance. */
export const createHomogeneousDecoder = <Item extends Unknown>(itemDecoder: Decoder<Item>): Decoder<Item[]> =>
(val: any) => {
const arrayResult = decodeArray(val);
if (isErrorMessage(arrayResult)) return arrayResult;
if (arrayResult.length === 0) return arrayResult;
const firstItemResult = itemDecoder(arrayResult[0]);
return isErrorMessage(firstItemResult)
? errorMessage(`For array: ${firstItemResult.error}`)
: val;
}
export const decodeValue = <value extends unknown>(value: value): Decoder<value> => (val: any) => (val === value) ? val : errorMessage(`Expecting ${value}`);
export const decodeNull = decodeValue(null);
export const decodeValues = <value extends unknown>(values: readonly value[]): Decoder<value> =>
(val: any) =>
values.indexOf(val) >= 0
? val
: errorMessage(`Actual ${val + ""}; Expecting: ${values.map(it => it + "").join(", ")}`);
export const decodeOr = <value1 extends unknown, value2 extends unknown>(value1: Decoder<value1>, value2: Decoder<value2>): Decoder<value1 | value2> =>
(val: any) => {
const value1Result = value1(val);
if (isSuccess(value1Result)) return value1Result;
const value2Result = value2(val);
return isSuccess(value2Result)
? value2Result
: errorMessage(`Failed to decode OR: ${value1Result.error}, ${value2Result.error}`);
}
export const decodeOr3 = <value1 extends unknown, value2 extends unknown, value3 extends unknown>(
value1: Decoder<value1>,
value2: Decoder<value2>,
value3: Decoder<value3>
): Decoder<value1 | value2 | value3> =>
(val: any) => {
const value1Result = value1(val);
if (isSuccess(value1Result)) return value1Result;
const value2Result = value2(val);
if (isSuccess(value2Result)) return value2Result;
const value3Result = value3(val);
return isSuccess(value3Result)
? value3Result
: errorMessage(`Failed to decode OR: ${value1Result.error}, ${value2Result.error}, ${value3Result.error}`);
}
}
module Example {
const decodeGender = DecoderApi.decodeValues(["male", "female", "unknown"] as const);
const decodePerson = DecoderApi.createObjectDecoder({
firstName: DecoderApi.decodeString,
gender: decodeGender,
lastName: DecoderApi.decodeString,
ageInYears: DecoderApi.decodeNumber,
isCool: DecoderApi.decodeBoolean
})
// decodeNumber the use of TypeOfTypePredicate to get ahold of the computed type.
type Person = DecoderApi.TypeOfDecoder<typeof decodePerson>;
const bob: Person = {
firstName: "Bob",
lastName: "Smith",
ageInYears: 30,
gender: "male",
isCool: true
}
const decodedPerson = decodePerson({
firstName: "John",
lastName: "Suder",
gender: "male",
ageInYears: 30,
isCool: false
});
if (DecoderApi.isSuccess(decodedPerson)) console.log(decodedPerson)
else console.error(decodedPerson.error);
const decodeCompany = DecoderApi.createObjectDecoder({
ceo: decodePerson,
coo: decodePerson,
cto: decodePerson,
workers: DecoderApi.createArrayDecoder(decodePerson)
})
type Company = DecoderApi.TypeOfDecoder<typeof decodeCompany>;
const decodedCompany = decodeCompany({
ceo: decodedPerson,
coo: decodedPerson,
cto: decodedPerson,
workers: [decodedPerson, 1]
});
if (DecoderApi.isSuccess(decodedCompany)) console.log(decodedCompany)
else console.error(decodedCompany.error);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment