Last active
July 1, 2016 22:49
-
-
Save unscriptable/323bcfc4e8aff5cabcf5 to your computer and use it in GitHub Desktop.
Compile-time enforcement of validation via type system via a container w/ smart constructors
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
'use strict'; | |
/*@flow*/ | |
/* | |
Here's an example of how to enforce at compile-time that an object is | |
validated before it is used in code where it could potentially fail at | |
run-time. | |
This one works by "containing" an object purely in another flow type. | |
(Sorta like a `newtype` in Haskell.) Actually, this code cheats; the | |
validation function casts to `any` to convince flow that "we know | |
what we're doing". This is silly, but works. | |
Also, this code forces the use of the container type throughout the | |
codebase. It'd be nicer if we could use the base type, instead. | |
(See next attempt.) | |
*/ | |
// The type of object we want to ensure is valid. | |
type Payment = { id?: number, userId: number, txid: number } | |
// A type alias for a validated object. | |
type CheckedPayment = Checked<true, Payment> | |
// An object that may or may not be validated to conform to the structure, T. | |
// This class should not be exported. We export a "smart constructor" instead. | |
declare class Checked<V:boolean, T> {} | |
// A smart constructor for Checked<x, Payment>. Creates an *un*checked payment. | |
export const createCheckedPayment | |
: (payment: Payment) => Checked<false, Payment> | |
= payment => payment | |
// A validator for Payment. Converts `Checked<false, Payment>` to | |
// `Checked<true, Payment>` if the validation tests pass. | |
export const validateCheckedPayment | |
: (object: Checked<false, Payment>) => CheckedPayment | |
= object => { | |
const o: any = object // oh, flow! | |
if (('id' in o && isNaN(o.id)) || isNaN(o.userId) || isNaN(o.txid)) { | |
throw new TypeError('Not a valid Payment') | |
} | |
return o | |
} | |
// Testing... | |
// Example of a function that only accepts a checked payment: | |
const doSomethingAwesome | |
: (o: CheckedPayment) => boolean | |
= o => true | |
// This example code typechecks because `good` is of type | |
// `Checked<true, Payment>` after successfully passing through | |
// `validateCheckedPayment`. | |
const good = validateCheckedPayment(createCheckedPayment({ | |
id: 42, userId: 27, txid: 1 | |
})) | |
doSomethingAwesome(good) | |
// This example doesn't typecheck since `createCheckedPayment` | |
// returns `Checked<false, Payment>` and `doSomethingAwesome` | |
// expects `Checked<true, Payment>`. | |
const bad = createCheckedPayment({ | |
id: 42, userId: 27, txid: 1 | |
}) | |
doSomethingAwesome(bad) |
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
'use strict'; | |
/*@flow*/ | |
/* | |
Here's an example of how to enforce at compile-time that an object is | |
validated before it is used in code where it could potentially fail at | |
run-time. | |
This one works by "containing" an object purely in another flow type. | |
(Sorta like a `newtype` in Haskell.) Actually, this code cheats; the | |
validation function casts to `any` to convince flow that "we know | |
what we're doing". This is silly, but works. | |
Unfortunately, there's no way to force the use of these types in flow. | |
The dev could easily just construct a compatible object and use it. | |
*/ | |
// The type of object we want to ensure is valid. | |
type Payment = { id?: number, userId: number, txid: number } | |
// An object that has not been validated to conform to the structure, T. | |
// This class should not be exported. We export a "smart constructor" instead. | |
declare class Unchecked<T> {} | |
// Smart constructor for Unchecked<Payment>. | |
export const createUncheckedPayment | |
: (object: Object) => Unchecked<Payment> | |
= (object) => object | |
// A validator for Payment. Converts `Unhecked<Payment>` to | |
// `Payment` if the validation tests pass. | |
export const validateCheckedPayment | |
: (unchecked: Unchecked<Payment>) => Payment | |
= (unchecked) => { | |
const o: any = unchecked | |
if (('id' in o && isNaN(o.id)) || isNaN(o.userId) || isNaN(o.txid)) { | |
throw new TypeError('Not a valid Payment') | |
} | |
return o | |
} | |
// Testing... | |
// Example of a function that only accepts a checked payment: | |
const doSomethingAwesome | |
: (o: Payment) => boolean | |
= (o) => true | |
// This example code typechecks because `good` is of type | |
// `Checked<true, Payment>` after successfully passing through | |
// `validateCheckedPayment`. | |
const good = validateCheckedPayment(createUncheckedPayment({ | |
id: 42, userId: 27, txid: 1 | |
})) | |
doSomethingAwesome(good) | |
// This example doesn't typecheck since `createCheckedPayment` | |
// returns `Checked<false, Payment>` and `doSomethingAwesome` | |
// expects `Checked<true, Payment>`. | |
const bad = createUncheckedPayment({ | |
id: 42, userId: 27, txid: 1 | |
}) | |
doSomethingAwesome(bad) |
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
'use strict'; | |
/*@flow*/ | |
/* | |
Here's an example of how to enforce at compile-time that an object is | |
validated before it is used in code where it could potentially fail at | |
run-time. | |
This one works by containing an object purely in another object. | |
Unfortunately, the error message could be a bit better. | |
Furthermore, there's no way to force the use of these types in flow. | |
The dev could easily just construct a compatible object and use it. | |
*/ | |
// The type of object we want to ensure is valid. | |
type Payment = { id?: number, userId: number, txid: number } | |
// An object that has not been validated to conform to the structure, T. | |
// This class should not be exported. We export a "smart constructor" instead. | |
declare class Unchecked<T> { object: T } | |
// Smart constructor for Unchecked<Payment>. | |
export const unchecked | |
= <T>(object: T): Unchecked<T> => | |
({ object }) | |
// : <T>(object: T) => Unchecked<T> | |
// = (object) => ({ object }) | |
export const createValidator | |
= <T>(validate: (obj:T)=>T): ((unchecked: Unchecked<T>) => T) => | |
unchecked => validate(unchecked.object) // throws if invalid | |
// Testing... | |
// Example of a function that only accepts a checked payment: | |
const doSomethingAwesome | |
: (o: Payment) => boolean | |
= o => true | |
// Example validator that converts Unchecked<Payment> to Payment. | |
const validatePayment | |
: (uncheckedPayment: Unchecked<Payment>) => Payment | |
= (uncheckedPayment) => createValidator(validateCheckedPayment) | |
// Validate a Payment. | |
export const validateCheckedPayment | |
: (payment: Payment) => Payment | |
= (payment) => { | |
if (('id' in payment && isNaN(payment.id)) || isNaN(payment.userId) || isNaN(payment.txid)) { | |
throw new TypeError('Not a valid Payment') | |
} | |
return payment | |
} | |
// This example code typechecks because `good` is of type | |
// `Checked<true, Payment>` after successfully passing through | |
// `validateCheckedPayment`. | |
const good = validatePayment(unchecked({ | |
id: 42, userId: 27, txid: 1 | |
})) | |
doSomethingAwesome(good) | |
// This example doesn't typecheck since `createCheckedPayment` | |
// returns `Checked<false, Payment>` and `doSomethingAwesome` | |
// expects `Checked<true, Payment>`. | |
const bad = unchecked({ | |
id: 42, userId: 27, txid: 1 | |
}) | |
doSomethingAwesome(bad) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment