From e187b54516b504501bcc847c42ce75a57ab3f3fb Mon Sep 17 00:00:00 2001 From: Vincent Driessen Date: Mon, 28 May 2018 10:57:40 +0200 Subject: [PATCH] Avoid using `mixed`s They're causing too many type incompatibility issues when decoders are used. This happened after decoders fully Flow Strict compatible. This new approach still uses "any"'s, but deliberately allows them. --- src/array.js | 6 +++--- src/boolean.js | 6 +++--- src/constants.js | 17 +++++++---------- src/dispatch.js | 4 ++-- src/either.js | 4 ++-- src/fail.js | 4 ++-- src/guard.js | 4 ++-- src/mapping.js | 2 +- src/number.js | 4 ++-- src/object.js | 22 ++++++++++++---------- src/optional.js | 4 ++-- src/string.js | 4 ++-- src/tuple.js | 4 ++-- src/types.js | 15 ++++++++++++--- src/utils.js | 11 +++++------ 15 files changed, 59 insertions(+), 52 deletions(-) diff --git a/src/array.js b/src/array.js index 90f74de7..971fd92c 100644 --- a/src/array.js +++ b/src/array.js @@ -3,14 +3,14 @@ import { annotate } from 'debrief'; import { Err, Ok } from 'lemons'; -import type { DecodeResult, Decoder } from './types'; +import type { DecodeResult, Decoder, anything } from './types'; import { compose } from './utils'; /** * Like a "Plain Old JavaScript Object", but for arrays: "Plain Old JavaScript * Array" ^_^ */ -export const poja: Decoder> = (blob: mixed) => { +export const poja: Decoder> = (blob: anything) => { if (!Array.isArray(blob)) { return Err(annotate(blob, 'Must be an array')); } @@ -23,7 +23,7 @@ export const poja: Decoder> = (blob: mixed) => { * encountered; or * - a new Ok with an array of all unwrapped Ok'ed values */ -function all(iterable: Array>, blobs: Array): DecodeResult> { +function all(iterable: Array>, blobs: anything): DecodeResult> { const results: Array = []; let index = 0; for (const result of iterable) { diff --git a/src/boolean.js b/src/boolean.js index 2ba6cce8..42ed0860 100644 --- a/src/boolean.js +++ b/src/boolean.js @@ -4,20 +4,20 @@ import { annotate } from 'debrief'; import { Err, Ok } from 'lemons'; import { number } from './number'; -import type { Decoder } from './types'; +import type { Decoder, anything } from './types'; import { map } from './utils'; /** * Decoder that only returns Ok for boolean inputs. Err otherwise. */ -export const boolean: Decoder = (blob: mixed) => { +export const boolean: Decoder = (blob: anything) => { return typeof blob === 'boolean' ? Ok(blob) : Err(annotate(blob, 'Must be boolean')); }; /** * Decoder that returns true for all truthy values, and false otherwise. Never fails. */ -export const truthy: Decoder = (blob: mixed) => { +export const truthy: Decoder = (blob: anything) => { return Ok(!!blob); }; diff --git a/src/constants.js b/src/constants.js index 988bc4cf..688af238 100644 --- a/src/constants.js +++ b/src/constants.js @@ -3,38 +3,35 @@ import { annotate } from 'debrief'; import { Err, Ok } from 'lemons'; -import type { Decoder } from './types'; +import type { Decoder, anything } from './types'; /** * Decoder that only returns Ok for `null` inputs. Err otherwise. */ -export const null_: Decoder = (blob: mixed) => (blob === null ? Ok(blob) : Err(annotate(blob, 'Must be null'))); +export const null_: Decoder = (blob: anything) => + blob === null ? Ok(blob) : Err(annotate(blob, 'Must be null')); /** * Decoder that only returns Ok for `undefined` inputs. Err otherwise. */ -export const undefined_: Decoder = (blob: mixed) => +export const undefined_: Decoder = (blob: anything) => blob === undefined ? Ok(blob) : Err(annotate(blob, 'Must be undefined')); /** * Decoder that only returns Ok for the given value constant. Err otherwise. */ export function constant(value: T): Decoder { - return (blob: mixed) => - blob === value - ? // $FlowFixMe - Potentially unsafe casting! - Ok(blob) - : Err(annotate(blob, `Must be constant ${String(value)}`)); + return (blob: anything) => (blob === value ? Ok(blob) : Err(annotate(blob, `Must be constant ${String(value)}`))); } /** * Decoder that always returns Ok for the given hardcoded value, no matter what the input. */ export function hardcoded(value: T): Decoder { - return (_: mixed) => Ok(value); + return (_: anything) => Ok(value); } /** * Decoder that always returns Ok for the given hardcoded value, no matter what the input. */ -export const mixed: Decoder = (blob: mixed) => Ok((blob: mixed)); +export const mixed: Decoder = (blob: anything) => Ok((blob: mixed)); diff --git a/src/dispatch.js b/src/dispatch.js index 94612a48..63b5c7a9 100644 --- a/src/dispatch.js +++ b/src/dispatch.js @@ -1,6 +1,6 @@ // @flow strict -import type { Decoder } from './types'; +import type { Decoder, anything } from './types'; /** * Given a function that takes a base decoder whose value will be passed to @@ -41,7 +41,7 @@ import type { Decoder } from './types'; * ); */ export function dispatch(base: Decoder, next: T => Decoder): Decoder { - return (blob: mixed) => + return (blob: anything) => // We'll dispatch on this value base(blob).andThen(value => // Now dispatch on the value by passing in T, and then invoking diff --git a/src/either.js b/src/either.js index 9859b13b..d91c928e 100644 --- a/src/either.js +++ b/src/either.js @@ -3,7 +3,7 @@ import { annotate, indent } from 'debrief'; import { Err, Ok } from 'lemons'; -import type { Decoder } from './types'; +import type { Decoder, anything } from './types'; /** * Indents and adds a dash in front of this (potentially multiline) string. @@ -14,7 +14,7 @@ function itemize(s: string = ''): string { } export function either(d1: Decoder, d2: Decoder): Decoder { - return (blob: mixed) => + return (blob: anything) => d1(blob).dispatch( value1 => Ok(value1), err1 => diff --git a/src/fail.js b/src/fail.js index 378e8555..f9b8489c 100644 --- a/src/fail.js +++ b/src/fail.js @@ -3,11 +3,11 @@ import { annotate } from 'debrief'; import { Err } from 'lemons'; -import type { Decoder } from './types'; +import type { Decoder, anything } from './types'; /** * Decoder that always fails with the given error message, no matter what the input. */ export function fail(msg: string): Decoder { - return (blob: mixed) => Err(annotate(blob, msg)); + return (blob: anything) => Err(annotate(blob, msg)); } diff --git a/src/guard.js b/src/guard.js index 082a797b..dea7a48f 100644 --- a/src/guard.js +++ b/src/guard.js @@ -2,10 +2,10 @@ import { serialize } from 'debrief'; -import type { Decoder, Guard } from './types'; +import type { Decoder, Guard, anything } from './types'; export function guard(decoder: Decoder): Guard { - return (blob: mixed) => + return (blob: anything) => decoder(blob) .mapError(annotation => { const err = new Error('\n' + serialize(annotation)); diff --git a/src/mapping.js b/src/mapping.js index c18f3a3e..a445ab49 100644 --- a/src/mapping.js +++ b/src/mapping.js @@ -20,7 +20,7 @@ import { compose } from './utils'; export function mapping(decoder: Decoder): Decoder> { return compose( pojo, - // $FlowIgnore - deliberately using Object here + // $FlowIgnore: deliberate use of Object here (blob: Object) => { let tuples: Array<[string, T]> = []; let errors: Array<[string, string | Annotation]> = []; diff --git a/src/number.js b/src/number.js index 9dc77502..91c59341 100644 --- a/src/number.js +++ b/src/number.js @@ -3,10 +3,10 @@ import { annotate } from 'debrief'; import { Err, Ok } from 'lemons'; -import type { Decoder } from './types'; +import type { Decoder, anything } from './types'; import { compose, predicate } from './utils'; -export const anyNumber: Decoder = (blob: mixed) => { +export const anyNumber: Decoder = (blob: anything) => { return typeof blob === 'number' && !Number.isNaN(blob) ? Ok(blob) : Err(annotate(blob, 'Must be number')); }; diff --git a/src/object.js b/src/object.js index 7ce2c291..769e0c8c 100644 --- a/src/object.js +++ b/src/object.js @@ -4,13 +4,13 @@ import { annotate, annotateFields, isAnnotation } from 'debrief'; import type { Annotation } from 'debrief'; import { Err, Ok } from 'lemons'; -import type { Decoder } from './types'; +import type { Decoder, anything } from './types'; import { compose, isDate } from './utils'; -// $FlowIgnore - helper to indicate we're deliberately using "any" +// $FlowIgnore: we're deliberately casting type cast = any; -function isObject(o: mixed): boolean %checks { +function isObject(o: anything): boolean %checks { return o !== null && typeof o === 'object' && !Array.isArray(o) && !isDate(o); } @@ -24,8 +24,8 @@ function subtract(xs: Set, ys: Set): Set { return result; } -// $FlowIgnore - deliberately using Object here -export const pojo: Decoder = (blob: mixed) => { +// $FlowIgnore: deliberate use of Object here +export const pojo: Decoder = (blob: anything) => { return isObject(blob) ? Ok(blob) : Err(annotate(blob, 'Must be an object')); }; @@ -55,11 +55,11 @@ type UnwrapDecoder = (Decoder) => T; * Put simply: it'll "peel off" all of the nested Decoders, puts them together * in an object, and wraps it in a Guard<...>. */ -export function object }>(mapping: O): Decoder<$ObjMap> { +export function object }>(mapping: O): Decoder<$ObjMap> { const known = new Set(Object.keys(mapping)); return compose( pojo, - // $FlowIgnore - deliberately using Object here + // $FlowIgnore: deliberate use of Object here (blob: Object) => { const actual = new Set(Object.keys(blob)); @@ -131,12 +131,14 @@ export function object }>(mapping: O): Dec ); } -export function exact }>(mapping: O): Decoder<$Exact<$ObjMap>> { +export function exact }>( + mapping: O +): Decoder<$Exact<$ObjMap>> { // Check the inputted object for any superfluous keys const allowed = new Set(Object.keys(mapping)); const checked = compose( pojo, - // $FlowIgnore - deliberately using Object here + // $FlowIgnore: deliberate use of Object here (blob: Object) => { const actual = new Set(Object.keys(blob)); const superfluous = subtract(actual, allowed); @@ -162,7 +164,7 @@ export function field(field: string, decoder: Decoder): Decoder { // like this, not efficient -- pull it out of this function) return compose( pojo, - // $FlowIgnore - deliberately using Object here + // $FlowIgnore: deliberate use of Object here (blob: Object) => { const value = blob[field]; const result = decoder(value); diff --git a/src/optional.js b/src/optional.js index 35d1d18b..3b56a8e9 100644 --- a/src/optional.js +++ b/src/optional.js @@ -5,14 +5,14 @@ import { Err, Ok } from 'lemons'; import { undefined_ } from './constants'; import { either } from './either'; -import type { Decoder } from './types'; +import type { Decoder, anything } from './types'; /** * Decoder that only returns Ok for `null` or `undefined` inputs. In both * cases, it will return `undefined`, so `null` inputs will get converted to * `undefined` outputs. Err otherwise. */ -export const undefined_or_null: Decoder = (blob: mixed) => +export const undefined_or_null: Decoder = (blob: anything) => blob === undefined || blob === null ? Ok(undefined) : Err(annotate(blob, 'Must be undefined or null')); /** diff --git a/src/string.js b/src/string.js index d42b1ad0..fbb1b084 100644 --- a/src/string.js +++ b/src/string.js @@ -3,7 +3,7 @@ import { annotate } from 'debrief'; import { Err, Ok } from 'lemons'; -import type { Decoder } from './types'; +import type { Decoder, anything } from './types'; import { compose, predicate } from './utils'; /** Match groups in this regex: @@ -21,7 +21,7 @@ const DEFAULT_SCHEMES = ['https']; /** * Decoder that only returns Ok for string inputs. Err otherwise. */ -export const string: Decoder = (blob: mixed) => { +export const string: Decoder = (blob: anything) => { return typeof blob === 'string' ? Ok(blob) : Err(annotate(blob, 'Must be string')); }; diff --git a/src/tuple.js b/src/tuple.js index 1f768330..738e75a8 100644 --- a/src/tuple.js +++ b/src/tuple.js @@ -3,14 +3,14 @@ import { annotate } from 'debrief'; import { Err, Ok } from 'lemons'; -import type { Decoder } from './types'; +import type { Decoder, anything } from './types'; /** * Builds a Decoder that returns Ok for 2-tuples of [T1, T2], given Decoders * for T1 and T2. Err otherwise. */ export function tuple2(decoder1: Decoder, decoder2: Decoder): Decoder<[T1, T2]> { - return (blobs: mixed) => { + return (blobs: anything) => { if (!Array.isArray(blobs)) { return Err(annotate(blobs, 'Must be an array')); } diff --git a/src/types.js b/src/types.js index 86770c98..c57d0504 100644 --- a/src/types.js +++ b/src/types.js @@ -3,8 +3,17 @@ import type { Annotation } from 'debrief'; import { Result } from 'lemons'; -export type Guard = mixed => T; +// NOTE: +// Normally, we should not be discarding Flow warnings about the use of the +// "any" type. But in the case of decoders, it's the very purpose of the +// library to accept *anything* as input. To avoid suppressing the "any" +// warnings everywhere throughout this library, we'll suppress it here once, +// and use this re-aliased version of "any" elsewhere. +// +// $FlowIgnore: decoders take *anything* as input. It's their purpose. +export type anything = any; + +export type Guard = anything => T; export type Predicate = T => boolean; export type DecodeResult = Result; -// $FlowIgnore - deliberately allow `any` here. It's the purpose of decoders! -export type Decoder = F => DecodeResult; +export type Decoder = F => DecodeResult; diff --git a/src/utils.js b/src/utils.js index 625c2852..534dad75 100644 --- a/src/utils.js +++ b/src/utils.js @@ -3,7 +3,7 @@ import { annotate } from 'debrief'; import { Err, Ok } from 'lemons'; -import type { Decoder } from './types'; +import type { Decoder, anything } from './types'; /** * This uses duck typing to check whether this is a Date instance. Since @@ -15,8 +15,7 @@ import type { Decoder } from './types'; * * But in this case, I chose the faster check. */ -export const isDate = (value: mixed): boolean => - !!value && value.getMonth !== undefined && typeof value.getMonth === 'function'; +export const isDate = (value: anything): boolean => !!value && typeof value.getMonth === 'function'; /** * Given a decoder T and a mapping function from T's to V's, returns a decoder @@ -32,15 +31,15 @@ export function map(decoder: Decoder, mapper: T => V): Decoder { /** * Compose two decoders by passing the result of the first into the second. * The second decoder may assume as its input type the output type of the first - * decoder (so it's not necessary to accept the typical "mixed"). This is - * useful for "narrowing down" the checks. For example, if you want to write + * decoder (so it's not necessary to accept the typical "any"). This is useful + * for "narrowing down" the checks. For example, if you want to write * a decoder for positive numbers, you can compose it from an existing decoder * for any number, and a decoder that, assuming a number, checks if it's * positive. Very often combined with the predicate() helper as the second * argument. */ export function compose(decoder: Decoder, next: Decoder): Decoder { - return (blob: mixed) => decoder(blob).andThen(next); + return (blob: anything) => decoder(blob).andThen(next); } /**