Dynamic object validation using $ObjMap in Flow
/* @flow */ | |
// A simplified representation of types using phantom types (so that we store the Type information both at value and type level) | |
class Type<T> {}; | |
class StringT extends Type<string> {} | |
class NumberT extends Type<number> {} | |
// A schema for a User | |
const User = { | |
name: new StringT(), | |
age: new NumberT() | |
}; | |
// function that checks whether a value conforms to a type representation | |
// this is a trivial implementation that uses instance checking | |
function isValid<V>(value: V, type: Type<any>): boolean { | |
if (type instanceof StringT) { | |
return typeof value === 'string'; | |
} else if (type instanceof NumberT) { | |
return typeof value === 'number'; | |
} | |
return false; | |
} | |
// a function that validates a JSON string against a schema | |
// the result type is either an Error or a transformation over the schema, where | |
// the values have been replaced with the actual values of the object. | |
// e.g. { age: 'number' } is mapped to { age: 42 } | |
function validate<O: { [_: string]: Type<any> }>(schema: O, value: string): Error | $ObjMap<O, <V>(_: Type<V>) => V> { | |
try { | |
const parsed = JSON.parse(value); | |
// for each key of the parsed object, check whether the type matches the expectations | |
Object.keys(parsed).forEach(k => { | |
const value = parsed[k]; | |
const expected = schema[k]; | |
if (!isValid(value, expected)) { | |
throw TypeError(`InvalidSchema. Expected ${k} of type ${expected}, got ${value} of type ${typeof value}`); | |
} | |
}); | |
// return the validated object | |
return parsed; | |
} catch (e) { | |
// reify the exception, returning it instead of throwing | |
return e; | |
} | |
} | |
const ok = validate(User, '{ "name": "gabro", "age": 42 }'); | |
const wrongSyntax = validate(User, '{ name": "gabro", "age": 42 }'); | |
const wrongType = validate(User, '{ "name": "gabro", "age": true }'); | |
printOrError(ok); // 42 | |
printOrError(wrongSyntax); // Invalid Syntax ... | |
printOrError(wrongType); // Invalid Schema ... | |
function printOrError(result) { | |
// Note that the instance check is crucial. The return type can be a validated object or an Error, so something like | |
// console.log(result.age) | |
// would result in type error, since we don't know yet whether it's one or the other. | |
if (result instanceof Error) { | |
const error = result; | |
console.log(error.message); | |
} else { | |
const { age, name } = result; | |
console.log(`${name} is ${age} years old`); | |
// also note that 'name' has type string and 'age' has type 'number' | |
// This won't typecheck | |
// console.log(age.trim()) | |
} | |
} |
This comment has been minimized.
This comment has been minimized.
Typechecks with flow v0.33+. Run with flow-node to get the output. |
This comment has been minimized.
This comment has been minimized.
This was a really helpful read–I've learned a lot from reading your and @gcanti's writing. Cheers! |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This comment has been minimized.
Thanks @gcanti for tips and tricks on encoding
Type