Package Exports
- cleaners
- cleaners/package.json
Readme
cleaners
Cleans & validates untrusted data, with TypeScript & Flow support
Do you ever read JSON data from the outside world? If you, you should probably clean & validate that data before you start using it. That can be a lot of work, so cleaners is here to help with:
- Validation - Ensuring that the input data matches the expected format.
- Cleaning - Inserting fallback values, parsing strings into Date objects, and so forth.
- Typing - Automatically generating TypeScript & Flow types.
If features:
- Zero external dependencies
- 100% test coverage
- 1K minified + gzip
Installing
If you are using Deno, just import cleaners directly:
import { asString } from 'https://deno.land/x/cleaners/mod.ts'If you are using Node, first install the package using npm i cleaners or yarn add cleaners, and then import it using either syntax:
// The oldschool way:
const { asString } = require('cleaners')
// Or using Node's new native module support:
import { asString } from 'cleaners'Overview
This library contains a collection of composable Cleaner functions. A cleaner function validates some incoming data, and either returns it with the proper type or throws an exception. Here are some simple examples:
import { asDate, asString } from 'cleaners'
const a = asString('hey') // Returns the string 'hey'
const b = asString(1) // Throws a TypeError
const c = asDate('2020-02-20') // Returns a Javascript Date objectTo handle objects, arrays, and other nested data types, this library includes some helpers for combining Cleaner functions together:
import { asArray, asObject, asOptional } from 'cleaners'
// Define a cleaner function for our custom object type:
const asMessage = asObject({
text: asString,
recipients: asArray(asString), // Array of strings
seenOn: asOptional(asDate), // Optional Date
replyCount: asOptional(asNumber, 0) // Number with default value
})
// Let's clean some network data:
try {
const response = await fetch('https://message-api')
const message = asMessage(await response.json())
} catch (error) {}Automatic type definitions
Thanks to our TypeScript & Flow support, the custom asMessage function above has a detailed return type. The means you will get the same error-checking & auto-completion as if you had entered the following type declaration by hand:
interface Message {
text: string
recipients: string[]
seenOn: Date | undefined
replyCount: number
}If you want to give names to these automatically-created types, use code like the following:
// Typescript:
type Message = ReturnType<typeof asMessage>
// Flow:
type Message = $Call<typeof asMessage>Exporting Cleaners in Flow
If you want to export cleaners between files in Flow, you may run into errors. This is because Flow requires explicit type definitions for all exports (unlike TypeScript):
// This works, since it's not exported:
const asNumbers = asArray(asNumber)
// Flow "Cannot build a typed interface for this module":
export const asNumbers = asArray(asNumber)
// This works again:
export const asNumbers: Cleaner<number[]> = asArray(asNumber)These explicit type definitions are redundant but not harmful, since Flow does check that they match the actual cleaner on the right.
Hand-written cleaners
Since cleaners are just functions, you can easily create your own as well, which is useful if you need extra data validation:
function asEvenNumber(raw: any): number {
if (typeof raw !== 'number' || raw % 2 !== 0) {
throw new TypeError('Expected an even number')
}
return raw
}Or extra data conversions:
import { asString, Cleaner } from 'cleaners'
import { base64 } from 'rfc4648'
const asBase64Data: Cleaner<Uint8Array> = raw => base64.parse(asString(raw))You can pass these functions to asObject or any of the others helpers, and they will work perfectly, including TypeScript & Flow return-type inference.
Basic cleaners
This library includes the following basic cleaner functions:
asBoolean- accepts & returns aboolean.asNumber- accepts & returns anumber.asString- accepts & returns astring.asDate- accepts & returns aDate, but parses strings if needed.asNull- accepts & returnsnull.asNone- accepts & returnsundefined, but acceptsnullas well.asUndefined- accepts & returnsundefined.asUnknown- accepts anything.
Compound cleaners
Compound cleaners don't clean data directly, but they create cleaners that can handle the data type. This library includes a few:
asArray- Builds an array cleaner.asObject- Builds an object cleaner.asOptional- Builds a cleaner for an item that might be undefined or null.asEither- Builds a cleaner for an item that might have multiple types.asMaybe- Builds a cleaner that quietly ignores invalid data.asJSON- Builds a cleaner for JSON strings.asMap- Deprecated alias forasObject.
asArray
asArray accepts a single Cleaner that applies to each item within the array:
// Makes a Cleaner<string[]>:
const asStringList = asArray(asString)asObject
asObject builds an object cleaner. The cleaner will accept any Javascript object and make a clean copy.
If asObject receives a single cleaner as its parameter, it will apply that cleaner to each property in the object. This is useful when objects act as key / value maps:
// Makes a Cleaner<{ [key: string]: number }>:
const asNumberMap = asObject(asNumber)
// Returns { a: 1, b: 2 }:
const a = asNumberMap({ a: 1, b: 2 })
// Throws "TypeError: Expected a number at .a":
const a = asNumberMap({ a: false })You can use asObject(asUnknown) if you just want to check that something is an object, and don't care what its contents are.
To clean an object with known property names, pass a "shape" object to asObject. Each propery in the "shape" object should be a cleaner that applies to the matching key in the input object. The cleaner won't copy any unknown properties:
// Makes a Cleaner<{ key: string }>:
const asThing = asObject({ key: asString })
// Returns { key: 'string' }, with "extra" removed:
const x = asThing({ key: 'string', extra: false })When asObject receives a shape argument, it also add it to the returned cleaner as a shape property. The shape property makes it possible to build bigger object cleaners out of smaller object cleaners:
const asBiggerThing = asObject({
extraProperty: asNumber,
// Also give BiggerThing has all the properties of Thing:
...asThing.shape
})In addition to the shape property, the returned cleaner will have a withRest method, which does the same thing as the cleaner, but also preserves unknown properties:
// Returns `{ key: 'string', extra: false }`,
// even though `asThing.shape` doesn't have an "extra" property:
const y = asThing.withRest({ key: 'string', extra: false })This is useful when you only want to clean some of the properties on an object, but not others.
asOptional
asOptional creates a cleaner that handles optional values. If the value to clean is null or undefined, it returns the fallback (which defaults to undefined). Otherwise, it cleans the value & returns it like normal:
// Makes a Cleaner<number>:
const asCounter = asOptional(asNumber, 0)
// Makes a Cleaner<number | void>:
const asMaybeNumber = asOptional(asNumber)
const a = asCounter(1) // returns 1
const b = asCounter(null) // returns 0
const b = asMaybeNumber(null) // returns undefinedasEither
asEither creates a cleaner that handles multiple options. It tries the first cleaner, and if that throws an exception, it tries the second cleaner:
// Makes a Cleaner<string | number>:
const asUnit = asEither(asString, asNumber)
const a = asUnit(1) // returns 1
const b = asUnit('1rem') // returns '1rem'
const c = asUnit(null) // Throws a TypeErrorasMaybe
asMaybe creates a cleaner that doesn't throw on an invalid type. It tries the cleaner, and if that throws an exception, it will return undefined instead:
// Makes a Cleaner<string | undefined>:
const asMaybeString = asMaybe(asString)
const a = asMaybe('Valid string') // returns 'Valid string'
const b = asMaybe(23) // returns undefined
const c = asMaybe(null) // returns undefinedThis cleaner is useful as a type guard on your data:
const pizza = asMaybe(asPizza)(obj)
const salad = asMaybe(asSalad)(obj)
if (pizza != null) {
// It's a pizza
} else if (salad != null) {
// It's a salad
} else {
// It's neither
}This type will silence all exceptions from the cleaner(s) it composes. Only use on types for which you do not care why a value is not valid.
asJSON
asJSON accepts a string, which it parses as JSON and passes to the nested cleaner:
// Makes a Cleaner<string[]>:
const asNamesFile = asJSON(asArray(asString))
const a = asNamesFile('["jack","jill"]') // returns ['jack', 'jill']
const b = asNamesFile([]) // TypeError: Expected a string
// Returns an array of strings, right from disk:
const names = asNamesFile(fs.readFileSync('names.json', 'utf8'))