Skip to content

Instantly share code, notes, and snippets.

@ccorcos
Created May 20, 2019 16:57
Show Gist options
  • Save ccorcos/88848fb34ad9da0d72199af0a9299142 to your computer and use it in GitHub Desktop.
Save ccorcos/88848fb34ad9da0d72199af0a9299142 to your computer and use it in GitHub Desktop.
import * as _ from "lodash"
/** This is how DataTypes are serialized. */
export type DataType =
| { type: "string" }
| { type: "number" }
| { type: "boolean" }
| { type: "literal"; value: string | number | boolean }
| { type: "array"; inner: DataType }
// Tuple types are not quite working yet.
// | { type: "tuple"; values: Array<DataType> }
| { type: "map"; inner: DataType }
| { type: "object"; properties: { [key: string]: DataType } }
| { type: "any" }
| { type: "optional"; inner: DataType }
| { type: "or"; values: Array<DataType> }
type DataTypeMap = { [K in DataType["type"]]: Extract<DataType, { type: K }> }
type IsDataTypeMap = {
[K in keyof DataTypeMap]: (
dataType: DataTypeMap[K],
value: unknown
) => boolean
}
function isPlainObject(value: unknown): value is {} {
return _.isPlainObject(value)
}
/** A map of DataType.type to validator functions. */
const isDataTypeMap: IsDataTypeMap = {
string: (dataType, value) => _.isString(value),
number: (dataType, value) => _.isNumber(value),
boolean: (dataType, value) => _.isBoolean(value),
literal: (dataType, value) => _.isEqual(value, dataType.value),
array: (dataType, value) =>
Array.isArray(value) &&
value.every(innerValue => {
return isDataType(dataType.inner, innerValue)
}),
// Tuple types are not quite working yet.
// tuple: (dataType, value) =>
// Array.isArray(value) &&
// value.length === dataType.values.length &&
// value.every((innerValue, index) => {
// return isDataType(dataType.values[index], innerValue)
// }),
map: (dataType, value) =>
isPlainObject(value) &&
Object.keys(value).every(_.isString) &&
Object.values(value).every(innerValue => {
return isDataType(dataType.inner, innerValue)
}),
object: (dataType, value) =>
isPlainObject(value) &&
Object.keys(value).every(key => {
return isDataType(dataType.properties[key], value[key])
}),
any: (dataType, value) => true,
or: (dataType, value) =>
dataType.values.some(possibleDataType => {
return isDataType(possibleDataType, value)
}),
optional: (dataType, value) =>
value === undefined || isDataType(dataType.inner, value),
}
/** Runtime validation for DataTypes. */
export function isDataType<T extends DataType>(dataType: T, value: unknown) {
const is = isDataTypeMap[dataType.type] as (
schema: DataType,
value: unknown
) => boolean
return is(dataType, value)
}
/**
* A runtime representation of a DataType that is serializable with runtime validation
* as well as TypeScript types available with `typeof DataType.value`.
*/
export class RuntimeDataType<T> {
value: T
dataType: DataType
constructor(dataType: DataType) {
this.dataType = dataType
}
/** Convenient wrapper for `isDataType`. */
is(value: unknown): value is T {
return isDataType(this.dataType, value)
}
toJSON() {
return this.dataType
}
}
// Runtime representations of each DataType.
export const string = new RuntimeDataType<string>({ type: "string" })
export const number = new RuntimeDataType<number>({ type: "number" })
export const boolean = new RuntimeDataType<boolean>({ type: "boolean" })
export function literal<T extends string | number>(x: T) {
return new RuntimeDataType<T>({ type: "literal", value: x })
}
export function optional<T>(inner: RuntimeDataType<T>) {
return new RuntimeDataType<T | undefined>({
type: "optional",
inner: inner.dataType,
})
}
export function array<T>(inner: RuntimeDataType<T>) {
return new RuntimeDataType<Array<T>>({
type: "array",
inner: inner.dataType,
})
}
// Tuple types are not quite working yet. I'm not sure how to make the generic
// a tuple of unwrapped values and then specify the argument as a tuple of
// wrapped values.
// export function tuple<T extends Array<RuntimeDataType<any>>>(...values: T) {
// return new RuntimeDataType<T>({
// type: "tuple",
// values: values.map(value => value.dataType),
// })
// }
export function map<T>(inner: RuntimeDataType<T>) {
return new RuntimeDataType<{ [key: string]: T }>({
type: "map",
inner: inner.dataType,
})
}
export function object<O extends { [key: string]: any }>(
schema: { [K in keyof O]: RuntimeDataType<O[K]> }
) {
const properties: { [key: string]: DataType } = {}
Object.keys(schema).forEach(key => {
properties[key] = schema[key].dataType
})
return new RuntimeDataType<O>({
type: "object",
properties: properties,
})
}
export const any = new RuntimeDataType<any>({ type: "any" })
export function or<T extends Array<RuntimeDataType<any>>>(...values: T) {
return new RuntimeDataType<T[number]["value"]>({
type: "or",
values: values.map(value => value.dataType),
})
}
// RuntimeDataTypes for DataTypes. Very Meta 🤯
// We're going to mutate this array to avoid circular references.
const dataTypeDataTypeValues: Array<any> = []
export const dataTypeDataType = new RuntimeDataType<DataType>({
type: "or",
values: dataTypeDataTypeValues,
})
const stringDataType = object({ type: literal("string") })
const numberDataType = object({ type: literal("number") })
const booleanDataType = object({ type: literal("boolean") })
const literalDataType = object({
type: literal("literal"),
value: or(string, number, boolean),
})
const arrayDataType = object({
type: literal("array"),
inner: dataTypeDataType,
})
// Tuple types are not quite working yet.
// const tupleDataType = object({
// type: literal("tuple"),
// values: array(dataTypeDataType),
// })
const mapDataType = object({
type: literal("map"),
inner: dataTypeDataType,
})
const objectDataType = object({
type: literal("object"),
properties: map(dataTypeDataType),
})
const anyDataType = object({ type: literal("any") })
const orDataType = object({
type: literal("or"),
values: array(dataTypeDataType),
})
const optionalDataType = object({
type: literal("optional"),
inner: dataTypeDataType,
})
// Specify all runtime type parameters
const runtimeDataTypeMap: {
[K in keyof DataTypeMap]: RuntimeDataType<DataTypeMap[K]>
} = {
string: stringDataType,
number: numberDataType,
boolean: booleanDataType,
literal: literalDataType,
array: arrayDataType,
// Tuple types are not quite working yet.
// tuple: tupleDataType,
map: mapDataType,
object: objectDataType,
any: anyDataType,
or: orDataType,
optional: optionalDataType,
}
// Type contrained! :)
dataTypeDataTypeValues.push(...Object.values(runtimeDataTypeMap))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment