Last active
December 23, 2021 03:41
-
-
Save dantman/dd48cef249d82353d37276d955a37a34 to your computer and use it in GitHub Desktop.
Basic TypeScript friendly validate/cast function based validation library experiment
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
import Axios, { AxiosError } from "axios"; | |
// Casting/validation library | |
class DataInputError extends Error { | |
path: string[] = []; | |
static isDataInputError(error: unknown): error is DataInputError { | |
return error ? error instanceof DataInputError : false; | |
} | |
wrapParent(propertyName: string): DataInputError { | |
const parent = new DataInputError(this.message); | |
parent.path = [propertyName, ...this.path]; | |
// @note Needs to also modify the message in some way to include and update the path | |
return parent; | |
} | |
} | |
function validateObject<TOut extends {}>(shape: { | |
[key in keyof TOut]: (data: unknown) => TOut[key]; | |
}): (data: unknown) => TOut { | |
return (data: unknown): TOut => { | |
if (data === null || typeof data !== "object") | |
throw new DataInputError("is not an object"); | |
const out: any = {}; | |
for (const key in shape) { | |
const propertyValidator = shape[key]; | |
try { | |
const outValue = propertyValidator(key in data ? data : undefined); | |
if (outValue !== undefined) out[key] = outValue; | |
} catch (e) { | |
if (DataInputError.isDataInputError(e)) { | |
throw e.wrapParent(key); | |
} else { | |
throw e; | |
} | |
} | |
} | |
return out; | |
}; | |
} | |
function castObject<TOut extends {}>(shape: { | |
[key in keyof TOut]: (data: unknown) => TOut[key]; | |
}): (data: unknown) => TOut { | |
const validate = validateObject(shape); | |
return (data: unknown): TOut => { | |
return validate(data === null || typeof data !== "object" ? {} : data); | |
}; | |
} | |
// Validation functions that can be user defined or provided by the library as helpers | |
function string(): (data: unknown) => string { | |
return () => { | |
if (typeof data !== "string") throw new DataInputError("is not a string"); | |
return data; | |
}; | |
} | |
// Wrappers could be used to add behaviours like optional properties | |
function optional<T>( | |
cast: (data: unknown) => T | |
): (data: unknown) => T | undefined { | |
return (data: unknown): T | undefined => { | |
return data == null ? undefined : cast(data); | |
}; | |
} | |
// Input data in an unknown format (query string data, form data, etc) | |
const data: unknown = { foo: { bar: "baz" } }; | |
// The only actual user code used for casting the data | |
const cast = castObject({ | |
foo: castObject({ | |
bar: optional(string()), | |
}), | |
error: (err) => (Axios.isAxiosError(err) ? err : undefined), | |
}); | |
const output = cast(data); | |
interface TheData { | |
foo: { bar?: string }; | |
error?: AxiosError; | |
} | |
const theData: TheData = output; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment