Last active
February 24, 2020 16:38
-
-
Save gabro/bb83ed574690645053b815da2082b937 to your computer and use it in GitHub Desktop.
Dynamic object validation using $ObjMap in Flow
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
/* @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()) | |
} | |
} |
Typechecks with flow v0.33+.
Run with flow-node to get the output.
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
Thanks @gcanti for tips and tricks on encoding
Type