Skip to content

Instantly share code, notes, and snippets.

@unscriptable
Last active July 1, 2016 22:49
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save unscriptable/323bcfc4e8aff5cabcf5 to your computer and use it in GitHub Desktop.
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
'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)
'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)
'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