Repository: gcanti/fp-ts
Pull request #502 (2018-06-27 07:22:16)

New Feature: add Option.getRefinement
This function can be used to ensure that a custom type definition is type safe.

Example

```ts
type A = { type: 'A' }
type B = { type: 'B' }
type C = A | B

const isA = (c: C): c is A => c.type === 'B' // <= typo but typescript doesn't complain
const isA = getRefinement<C, A>(c => (c.type === 'B' ? some(c) : none)) // static error: Type '"B"' is not assignable to type '"A"'
```

The trick is to force typescript to check that the refinement is correct in `c.type === 'B' ? some(c) : none`


Patch:
diff --git a/package.json b/package.json
index 21701bc3..bef60333 100644
--- a/package.json
+++ b/package.json
@@ -13,7 +13,7 @@
     "jest-coverage": "jest --ci --coverage",
     "prettier": "prettier --list-different \"./{src,test,examples,exercises}/**/*.ts\"",
     "fix-prettier": "prettier --write \"./{src,test,examples,exercises}/**/*.ts\"",
-    "typings": "typings-checker --project ./typings-checker/tsconfig.json ./typings-checker/index.ts",
+    "typings": "typings-checker --allow-expect-error --project ./typings-checker/tsconfig.json ./typings-checker/index.ts",
     "test": "npm run lint && npm run prettier && npm run typings && npm run jest",
     "clean": "rimraf lib/*",
     "build": "npm run clean && tsc",
diff --git a/src/Apply.ts b/src/Apply.ts
index 613caeac..8f1f9564 100644
--- a/src/Apply.ts
+++ b/src/Apply.ts
@@ -340,11 +340,11 @@ export function sequenceT<F extends URIS2, L>(F: Apply2C<F, L>): SequenceT2C<F,
 export function sequenceT<F extends URIS>(F: Apply1<F>): SequenceT1<F>
 export function sequenceT<F>(F: Apply<F>): SequenceT<F>
 export function sequenceT<F>(F: Apply<F>): SequenceT<F> {
-  return (...args: any[]) => {
+  return (...args: Array<any>) => {
     const len = args.length
     let f = tupleConstructors[len]
     if (!Boolean(f)) {
-      f = tupleConstructors[len] = curried((...args: any[]): any[] => args, len - 1, [])
+      f = tupleConstructors[len] = curried((...args: Array<any>): Array<any> => args, len - 1, [])
     }
     let r = F.map(args[0], f)
     for (let i = 1; i < len; i++) {
diff --git a/src/Array.ts b/src/Array.ts
index f4988563..7eaa64da 100644
--- a/src/Array.ts
+++ b/src/Array.ts
@@ -57,7 +57,7 @@ export const getMonoid = <A = never>(): Monoid<Array<A>> => {
  * @constant
  * @since 1.0.0
  */
-export const getSetoid: <A>(S: Setoid<A>) => Setoid<A[]> = getArraySetoid
+export const getSetoid: <A>(S: Setoid<A>) => Setoid<Array<A>> = getArraySetoid
 
 /**
  * Derives an Order over the Array of a given element type from the Order, 'O', of that type. The ordering between two
@@ -742,8 +742,8 @@ export const sortBy1 = <A>(head: Ord<A>, tail: Array<Ord<A>>): Endomorphism<Arra
   return sort(tail.reduce(getSemigroup<A>().concat, head))
 }
 
-const filterMap = <A, B>(fa: A[], f: (a: A) => Option<B>): B[] => {
-  const result: B[] = []
+const filterMap = <A, B>(fa: Array<A>, f: (a: A) => Option<B>): Array<B> => {
+  const result: Array<B> = []
   for (const a of fa) {
     const optionB = f(a)
     if (optionB.isSome()) {
@@ -756,9 +756,9 @@ const filterMap = <A, B>(fa: A[], f: (a: A) => Option<B>): B[] => {
  * @function
  * @since 1.0.0
  */
-export const partitionMap = <A, L, R>(fa: A[], f: (a: A) => Either<L, R>): Separated<L[], R[]> => {
-  const left: L[] = []
-  const right: R[] = []
+export const partitionMap = <A, L, R>(fa: Array<A>, f: (a: A) => Either<L, R>): Separated<Array<L>, Array<R>> => {
+  const left: Array<L> = []
+  const right: Array<R> = []
   for (const a of fa) {
     const e = f(a)
     if (e.isLeft()) {
@@ -777,10 +777,10 @@ export const partitionMap = <A, L, R>(fa: A[], f: (a: A) => Either<L, R>): Separ
  * @function
  * @since 1.0.0
  */
-export const filter = <A>(fa: A[], p: Predicate<A>): A[] => fa.filter(p)
-const partition = <A>(fa: A[], p: Predicate<A>): Separated<A[], A[]> => {
-  const left: A[] = []
-  const right: A[] = []
+export const filter = <A>(fa: Array<A>, p: Predicate<A>): Array<A> => fa.filter(p)
+const partition = <A>(fa: Array<A>, p: Predicate<A>): Separated<Array<A>, Array<A>> => {
+  const left: Array<A> = []
+  const right: Array<A> = []
   for (const a of fa) {
     if (p(a)) {
       right.push(a)
@@ -793,8 +793,8 @@ const partition = <A>(fa: A[], p: Predicate<A>): Separated<A[], A[]> => {
     right
   }
 }
-const compact = <A>(fa: Option<A>[]): A[] => {
-  const result: A[] = []
+const compact = <A>(fa: Array<Option<A>>): Array<A> => {
+  const result: Array<A> = []
   for (const optionA of fa) {
     if (optionA.isSome()) {
       result.push(optionA.value)
@@ -802,9 +802,9 @@ const compact = <A>(fa: Option<A>[]): A[] => {
   }
   return result
 }
-const separate = <RL, RR>(fa: Either<RL, RR>[]): Separated<RL[], RR[]> => {
-  const left: RL[] = []
-  const right: RR[] = []
+const separate = <RL, RR>(fa: Array<Either<RL, RR>>): Separated<Array<RL>, Array<RR>> => {
+  const left: Array<RL> = []
+  const right: Array<RR> = []
   for (const e of fa) {
     if (e.isLeft()) {
       left.push(e.value)
@@ -834,14 +834,14 @@ export const catOptions = compact
  */
 export const mapOption = filterMap
 
-const wither = <F>(F: Applicative<F>): (<A, B>(ta: A[], f: (a: A) => HKT<F, Option<B>>) => HKT<F, B[]>) => {
+const wither = <F>(F: Applicative<F>): (<A, B>(ta: Array<A>, f: (a: A) => HKT<F, Option<B>>) => HKT<F, Array<B>>) => {
   const traverseF = traverse(F)
   return (wa, f) => F.map(traverseF(wa, f), compact)
 }
 
 const wilt = <F>(
   F: Applicative<F>
-): (<RL, RR, A>(wa: A[], f: (a: A) => HKT<F, Either<RL, RR>>) => HKT<F, Separated<RL[], RR[]>>) => {
+): (<RL, RR, A>(wa: Array<A>, f: (a: A) => HKT<F, Either<RL, RR>>) => HKT<F, Separated<Array<RL>, Array<RR>>>) => {
   const traverseF = traverse(F)
   return (wa, f) => F.map(traverseF(wa, f), separate)
 }
diff --git a/src/Monoid.ts b/src/Monoid.ts
index ec86f422..1021c29c 100644
--- a/src/Monoid.ts
+++ b/src/Monoid.ts
@@ -71,7 +71,7 @@ export const monoidAny: Monoid<boolean> = {
   empty: false
 }
 
-const emptyArray: any[] = []
+const emptyArray: Array<any> = []
 
 /**
  * @instance
diff --git a/src/Option.ts b/src/Option.ts
index 3c9e7a0f..36cd4333 100644
--- a/src/Option.ts
+++ b/src/Option.ts
@@ -557,6 +557,26 @@ export const isNone = <A>(fa: Option<A>): fa is None<A> => {
 export const fromRefinement = <A, B extends A>(refinement: Refinement<A, B>) => (a: A): Option<B> => {
   return refinement(a) ? some(a) : none
 }
+
+/**
+ * Returns a refinement from a prism.
+ * This function ensures that a custom type guard definition is type-safe.
+ *
+ * @example
+ * type A = { type: 'A' }
+ * type B = { type: 'B' }
+ * type C = A | B
+ *
+ * const isA = (c: C): c is A => c.type === 'B' // <= typo but typescript doesn't complain
+ * const isA = getRefinement<C, A>(c => (c.type === 'B' ? some(c) : none)) // static error: Type '"B"' is not assignable to type '"A"'
+ *
+ * @function
+ * @since 1.7.0
+ */
+export const getRefinement = <A, B extends A>(getOption: (a: A) => Option<B>): Refinement<A, B> => {
+  return (a: A): a is B => getOption(a).isSome()
+}
+
 const compact = <A>(fa: Option<Option<A>>): Option<A> => fa.chain(identity)
 const separate = <RL, RR>(fa: Option<Either<RL, RR>>): Separated<Option<RL>, Option<RR>> => {
   if (fa.isNone()) {
diff --git a/src/Set.ts b/src/Set.ts
index 6df767a1..57a95b9b 100644
--- a/src/Set.ts
+++ b/src/Set.ts
@@ -281,7 +281,7 @@ export const remove = <A>(S: Setoid<A>) => (a: A, x: Set<A>): Set<A> => {
  * @function
  * @since 1.2.0
  */
-export const fromArray = <A>(S: Setoid<A>) => (as: A[]): Set<A> => {
+export const fromArray = <A>(S: Setoid<A>) => (as: Array<A>): Set<A> => {
   const len = as.length
   const r = new Set<A>()
   const isMember = member(S)(r)
diff --git a/src/Tree.ts b/src/Tree.ts
index 1e424926..bd950d93 100644
--- a/src/Tree.ts
+++ b/src/Tree.ts
@@ -128,7 +128,7 @@ const reduce = <A, B>(fa: Tree<A>, b: B, f: (b: B, a: A) => B): B => {
 
 const traverse = <F>(F: Applicative<F>): (<A, B>(ta: Tree<A>, f: (a: A) => HKT<F, B>) => HKT<F, Tree<B>>) => {
   const traverseAF = traverseA(F)
-  const treeLifted: <A>(a: HKT<F, A>) => (b: HKT<F, Tree<A>[]>) => HKT<F, Tree<A>> = liftA2(F)(
+  const treeLifted: <A>(a: HKT<F, A>) => (b: HKT<F, Array<Tree<A>>>) => HKT<F, Tree<A>> = liftA2(F)(
     <A>(value: A) => (forest: Forest<A>): Tree<A> => new Tree(value, forest)
   )
   const r = <A, B>(ta: Tree<A>, f: (a: A) => HKT<F, B>): HKT<F, Tree<B>> =>
@@ -141,7 +141,7 @@ const traverse = <F>(F: Applicative<F>): (<A, B>(ta: Tree<A>, f: (a: A) => HKT<F
  * @since 1.6.0
  */
 export const getSetoid = <A>(S: Setoid<A>): Setoid<Tree<A>> => {
-  let SA: Setoid<Tree<A>[]>
+  let SA: Setoid<Array<Tree<A>>>
   const R: Setoid<Tree<A>> = {
     equals: (x, y) => S.equals(x.value, y.value) && SA.equals(x.forest, y.forest)
   }
@@ -307,7 +307,7 @@ export function unfoldForestM<M>(
   M: Monad<M>
 ): <A, B>(bs: Array<B>, f: (b: B) => HKT<M, [A, Array<B>]>) => HKT<M, Forest<A>> {
   const traverseAM = traverseA(M)
-  let unfoldTreeMM: <A, B>(b: B, f: (b: B) => HKT<M, [A, B[]]>) => HKT<M, Tree<A>>
+  let unfoldTreeMM: <A, B>(b: B, f: (b: B) => HKT<M, [A, Array<B>]>) => HKT<M, Tree<A>>
   return (bs, f) => {
     // tslint:disable-next-line
     if (unfoldTreeMM === undefined) {
diff --git a/tslint.json b/tslint.json
index f02598c6..6535265f 100644
--- a/tslint.json
+++ b/tslint.json
@@ -8,6 +8,7 @@
     "ter-indent": false,
     "strict-boolean-expressions": true,
     "forin": true,
-    "no-console": true
+    "no-console": true,
+    "array-type": [true, "generic"]
   }
 }
diff --git a/typings-checker/index.ts b/typings-checker/index.ts
index effb3ede..ef4df933 100644
--- a/typings-checker/index.ts
+++ b/typings-checker/index.ts
@@ -6,7 +6,7 @@ import { Const, const_ } from '../src/Const'
 import { Either, either } from '../src/Either'
 import { Functor2C, Functor3C, lift } from '../src/Functor'
 import { getMonad as getIxIOMonad } from '../src/IxIO'
-import { Option, option } from '../src/Option'
+import { Option, option, getRefinement, some, none } from '../src/Option'
 import * as optionT from '../src/OptionT'
 import { Reader, reader } from '../src/Reader'
 import { getArraySemigroup, semigroupString } from '../src/Semigroup'
@@ -99,3 +99,11 @@ declare function apiForTaskify(path: string, callback: (err: Error | null | unde
 const apiTaskified = taskify(apiForTaskify)
 
 type S1 = AssertEquals<typeof apiTaskified, (a: string) => TaskEither<Error, string>, 'T'>
+
+// getRefinement
+
+type A = { type: 'A' }
+type B = { type: 'B' }
+type C = A | B
+// $ExpectError Type '"B"' is not assignable to type '"A"'
+const isA = getRefinement<C, A>(c => c.type === 'B' ? some(c) : none)


Test patch:
diff --git a/test/Option.ts b/test/Option.ts
index 4be115f2..6c2b34fe 100644
--- a/test/Option.ts
+++ b/test/Option.ts
@@ -17,7 +17,8 @@ import {
   none,
   option,
   some,
-  tryCatch
+  tryCatch,
+  getRefinement
 } from '../src/Option'
 import { ordString } from '../src/Ord'
 import { semigroupString } from '../src/Semigroup'
@@ -339,4 +340,17 @@ describe('Option', () => {
     assert.deepEqual(wiltIdentity(some(1), f), new Identity({ left: some(0), right: none }))
     assert.deepEqual(wiltIdentity(some(3), f), new Identity({ left: none, right: some(4) }))
   })
+
+  it('getRefinement', () => {
+    const f = (s: string | number): Option<string> => (typeof s === 'string' ? some(s) : none)
+    const isString = getRefinement(f)
+    assert.strictEqual(isString('s'), true)
+    assert.strictEqual(isString(1), false)
+    type A = { type: 'A' }
+    type B = { type: 'B' }
+    type C = A | B
+    const isA = getRefinement<C, A>(c => (c.type === 'A' ? some(c) : none))
+    assert.strictEqual(isA({ type: 'A' }), true)
+    assert.strictEqual(isA({ type: 'B' }), false)
+  })
 })