Repository: gcanti/io-ts
Pull request #333 (2019-06-12 12:59:56)

deprecate `taggedUnion` in favour of `union`


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e86cfc7..d93fac9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -14,6 +14,13 @@
 **Note**: Gaps between patch versions are faulty/broken releases. **Note**: A feature tagged as Experimental is in a
 high state of flux, you're at risk of it changing without notice.
 
+# 1.9.0
+
+- **New Feature**
+  - `union` is now able to detect and optimize tagged unions (@gcanti)
+- **Deprecation**
+  - deprecate `taggedUnion` in favour of `union` (@gcanti)
+
 # 1.8.6
 
 output ES modules to better support tree-shaking, closes #326 (@gcanti)
diff --git a/README.md b/README.md
index e04eda3..22c7bd3 100644
--- a/README.md
+++ b/README.md
@@ -17,7 +17,6 @@ Table of contents
 - [Implemented types / combinators](#implemented-types--combinators)
 - [Recursive types](#recursive-types)
   - [Mutually recursive types](#mutually-recursive-types)
-- [Tagged unions](#tagged-unions)
 - [Branded types / Refinements](#branded-types--refinements)
 - [Exact types](#exact-types)
 - [Mixing required and optional props](#mixing-required-and-optional-props)
@@ -250,7 +249,7 @@ type User = {
 | readonly array              | `ReadonlyArray<A>`          | `t.readonlyArray(A)`                                                 |
 | type alias                  | `type T = { name: A }`      | `t.type({ name: A })`                                                |
 | tuple                       | `[ A, B ]`                  | `t.tuple([ A, B ])`                                                  |
-| union                       | `A \| B`                    | `t.union([ A, B ])` or `t.taggedUnion(tag, [ A, B ])`                |
+| union                       | `A \| B`                    | `t.union([ A, B ])`                                                  |
 | intersection                | `A & B`                     | `t.intersection([ A, B ])`                                           |
 | keyof                       | `keyof M`                   | `t.keyof(M)` (**only supports string keys**)                         |
 | recursive types             | ✘                           | `t.recursion(name, definition)`                                      |
@@ -305,26 +304,6 @@ const Bar: t.Type<Bar> = t.recursion('Bar', () =>
 )
 ```
 
-# Tagged unions
-
-If you are encoding tagged unions, instead of the general purpose `union` combinator, you may want to use the
-`taggedUnion` combinator in order to get better performances
-
-```ts
-const A = t.type({
-  tag: t.literal('A'),
-  foo: t.string
-})
-
-const B = t.type({
-  tag: t.literal('B'),
-  bar: t.number
-})
-
-// the actual presence of the tag is statically checked
-const U = t.taggedUnion('tag', [A, B])
-```
-
 # Branded types / Refinements
 
 You can brand / refine a codec (_any_ codec) using the `brand` combinator
diff --git a/docs/modules/index.ts.md b/docs/modules/index.ts.md
index e7f0acd..02224b5 100644
--- a/docs/modules/index.ts.md
+++ b/docs/modules/index.ts.md
@@ -46,7 +46,7 @@ parent: Modules
 - [~~TaggedIntersection~~ (interface)](#taggedintersection-interface)
 - [~~TaggedRefinement~~ (interface)](#taggedrefinement-interface)
 - [~~TaggedUnion~~ (interface)](#taggedunion-interface)
-- [TaggedUnionC (interface)](#taggedunionc-interface)
+- [~~TaggedUnionC~~ (interface)](#taggedunionc-interface)
 - [TupleC (interface)](#tuplec-interface)
 - [TypeC (interface)](#typec-interface)
 - [UndefinedC (interface)](#undefinedc-interface)
@@ -103,7 +103,7 @@ parent: Modules
 - [RefinementType (class)](#refinementtype-class)
 - [~~StrictType~~ (class)](#stricttype-class)
 - [StringType (class)](#stringtype-class)
-- [TaggedUnionType (class)](#taggeduniontype-class)
+- [~~TaggedUnionType~~ (class)](#taggeduniontype-class)
 - [TupleType (class)](#tupletype-class)
 - [Type (class)](#type-class)
   - [pipe (method)](#pipe-method)
@@ -154,7 +154,7 @@ parent: Modules
 - [~~refinement~~ (function)](#refinement-function)
 - [strict (function)](#strict-function)
 - [success (function)](#success-function)
-- [taggedUnion (function)](#taggedunion-function)
+- [~~taggedUnion~~ (function)](#taggedunion-function)
 - [tuple (function)](#tuple-function)
 - [type (function)](#type-function)
 - [union (function)](#union-function)
@@ -600,12 +600,12 @@ export interface TaggedUnion<Tag extends string, A, O = A> extends UnionType<Arr
 
 Added in v1.3.0
 
-# TaggedUnionC (interface)
+# ~~TaggedUnionC~~ (interface)
 
 **Signature**
 
 ```ts
-export interface TaggedUnionC<Tag extends string, CS extends [Mixed, Mixed, ...Array<Mixed>]>
+export interface TaggedUnionC<Tag extends string, CS extends [Mixed, Mixed, ...Array<Mixed>]>  // tslint:disable-next-line: deprecation
   extends TaggedUnionType<Tag, CS, TypeOf<CS[number]>, OutputOf<CS[number]>, unknown> {}
 ```
 
@@ -1413,7 +1413,7 @@ export class StringType {
 
 Added in v1.0.0
 
-# TaggedUnionType (class)
+# ~~TaggedUnionType~~ (class)
 
 **Signature**
 
@@ -1421,8 +1421,11 @@ Added in v1.0.0
 export class TaggedUnionType<Tag, CS, A, O, I> {
   constructor(
     name: string,
+    // tslint:disable-next-line: deprecation
     is: TaggedUnionType<Tag, CS, A, O, I>['is'],
+    // tslint:disable-next-line: deprecation
     validate: TaggedUnionType<Tag, CS, A, O, I>['validate'],
+    // tslint:disable-next-line: deprecation
     encode: TaggedUnionType<Tag, CS, A, O, I>['encode'],
     codecs: CS,
     readonly tag: Tag
@@ -2070,7 +2073,9 @@ export const success = <T>(value: T): Validation<T> => ...
 
 Added in v1.0.0
 
-# taggedUnion (function)
+# ~~taggedUnion~~ (function)
+
+Use `union` instead
 
 **Signature**
 
@@ -2079,6 +2084,7 @@ export const taggedUnion = <Tag extends string, CS extends [Mixed, Mixed, ...Arr
   tag: Tag,
   codecs: CS,
   name: string = getUnionName(codecs)
+  // tslint:disable-next-line: deprecation
 ): TaggedUnionC<Tag, CS> => ...
 ```
 
diff --git a/package.json b/package.json
index 32e0c7a..f2db20f 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "io-ts",
-  "version": "1.8.6",
+  "version": "1.9.0",
   "description": "TypeScript compatible runtime type system for IO validation",
   "files": [
     "lib",
diff --git a/src/index.ts b/src/index.ts
index 7e65e35..8f3fa18 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,6 +1,5 @@
 import { Either, Left, Right } from 'fp-ts/lib/Either'
 import { Predicate, Refinement } from 'fp-ts/lib/function'
-import { Monoid } from 'fp-ts/lib/Monoid'
 
 /**
  * @since 1.0.0
@@ -208,25 +207,10 @@ const isUnknownCodec = getIsCodec<UnknownType>('UnknownType')
 // tslint:disable-next-line: deprecation
 const isAnyCodec = getIsCodec<AnyType>('AnyType')
 
-const isLiteralCodec = getIsCodec<LiteralType<LiteralValue>>('LiteralType')
-
 const isInterfaceCodec = getIsCodec<InterfaceType<Props>>('InterfaceType')
 
 const isPartialCodec = getIsCodec<PartialType<Props>>('PartialType')
 
-// tslint:disable-next-line: deprecation
-const isStrictCodec = getIsCodec<StrictType<Props>>('StrictType')
-
-const isIntersectionCodec = getIsCodec<IntersectionType<Array<Any>>>('IntersectionType')
-
-const isUnionCodec = getIsCodec<UnionType<Array<Any>>>('UnionType')
-
-const isExactCodec = getIsCodec<ExactType<Any>>('ExactType')
-
-const isRefinementCodec = getIsCodec<RefinementType<Any>>('RefinementType')
-
-const isRecursiveCodec = getIsCodec<RecursiveType<Any>>('RecursiveType')
-
 //
 // basic types
 //
@@ -604,8 +588,6 @@ export const keyof = <D extends { [key: string]: unknown }>(
  */
 export class RecursiveType<C extends Any, A = any, O = A, I = unknown> extends Type<A, O, I> {
   readonly _tag: 'RecursiveType' = 'RecursiveType'
-  /** @internal */
-  getIndexRecord!: () => IndexRecord
   constructor(
     name: string,
     is: RecursiveType<C, A, O, I>['is'],
@@ -642,15 +624,6 @@ export const recursion = <A, O = A, I = unknown, C extends Type<A, O, I> = Type<
     a => runDefinition().encode(a),
     runDefinition
   )
-  let indexRecordCache: IndexRecord
-  Self.getIndexRecord = () => {
-    if (!indexRecordCache) {
-      isRecursiveCodecIndexable = false
-      indexRecordCache = getCodecIndexRecord(definition(Self), Self, Self)
-      isRecursiveCodecIndexable = true
-    }
-    return indexRecordCache
-  }
   return Self
 }
 
@@ -740,8 +713,8 @@ const getNameFromProps = (props: Props): string =>
     .map(k => `${k}: ${props[k].name}`)
     .join(', ')
 
-const useIdentity = (codecs: Array<Any>, len: number): boolean => {
-  for (let i = 0; i < len; i++) {
+const useIdentity = (codecs: Array<Any>): boolean => {
+  for (let i = 0; i < codecs.length; i++) {
     if (codecs[i].encode !== identity) {
       return false
     }
@@ -832,7 +805,7 @@ export const type = <P extends Props>(props: P, name: string = getInterfaceTypeN
       }
       return errors.length > 0 ? failures(errors) : success(a as any)
     },
-    useIdentity(types, len)
+    useIdentity(types)
       ? identity
       : a => {
           const s: { [x: string]: any } = { ...a }
@@ -938,7 +911,7 @@ export const partial = <P extends Props>(
       }
       return errors.length > 0 ? failures(errors) : success(a as any)
     },
-    useIdentity(types, len)
+    useIdentity(types)
       ? identity
       : a => {
           const s: { [key: string]: any } = { ...a }
@@ -1095,36 +1068,85 @@ export const union = <CS extends [Mixed, Mixed, ...Array<Mixed>]>(
   codecs: CS,
   name: string = getUnionName(codecs)
 ): UnionC<CS> => {
-  const len = codecs.length
-  return new UnionType(
-    name,
-    (u): u is TypeOf<CS[number]> => codecs.some(type => type.is(u)),
-    (u, c) => {
-      const errors: Errors = []
+  const index = getIndex(codecs)
+  if (index !== undefined && codecs.length > 0) {
+    const [tag, groups] = index
+    const len = groups.length
+    const find = (value: any): number | undefined => {
       for (let i = 0; i < len; i++) {
-        const type = codecs[i]
-        const validation = type.validate(u, appendContext(c, String(i), type, u))
-        if (validation.isRight()) {
-          return validation
+        if (groups[i].indexOf(value) !== -1) {
+          return i
         }
-        pushAll(errors, validation.value)
       }
-      return errors.length > 0 ? failures(errors) : failure(u, c)
-    },
-    useIdentity(codecs, len)
-      ? identity
-      : a => {
-          for (let i = 0; i < len; i++) {
-            const codec = codecs[i]
-            if (codec.is(a)) {
-              return codec.encode(a)
+      return undefined
+    }
+    // tslint:disable-next-line: deprecation
+    return new TaggedUnionType(
+      name,
+      (u): u is TypeOf<CS[number]> => {
+        if (!UnknownRecord.is(u)) {
+          return false
+        }
+        const i = find(u[tag])
+        return i !== undefined ? codecs[i].is(u) : false
+      },
+      (u, c) => {
+        const dictionaryResult = UnknownRecord.validate(u, c)
+        if (dictionaryResult.isLeft()) {
+          return dictionaryResult
+        }
+        const r = dictionaryResult.value
+        const i = find(r[tag])
+        if (i === undefined) {
+          return failure(u, c)
+        }
+        const codec = codecs[i]
+        return codec.validate(r, appendContext(c, String(i), codec, r))
+      },
+      useIdentity(codecs)
+        ? identity
+        : a => {
+            const i = find(a[tag])
+            if (i === undefined) {
+              // https://github.com/gcanti/io-ts/pull/305
+              throw new Error(`no codec found to encode value in union codec ${name}`)
+            } else {
+              return codecs[i].encode(a)
             }
+          },
+      codecs,
+      tag
+    )
+  } else {
+    return new UnionType(
+      name,
+      (u): u is TypeOf<CS[number]> => codecs.some(type => type.is(u)),
+      (u, c) => {
+        const errors: Errors = []
+        for (let i = 0; i < codecs.length; i++) {
+          const codec = codecs[i]
+          const validation = codec.validate(u, appendContext(c, String(i), codec, u))
+          if (validation.isRight()) {
+            return validation
           }
-          // https://github.com/gcanti/io-ts/pull/305
-          throw new Error(`no codec found to encode value in union type ${name}`)
-        },
-    codecs
-  )
+          pushAll(errors, validation.value)
+        }
+        return failures(errors)
+      },
+      useIdentity(codecs)
+        ? identity
+        : a => {
+            for (const codec of codecs) {
+              if (codec.is(a)) {
+                return codec.encode(a)
+              }
+            }
+            // https://github.com/gcanti/io-ts/pull/305
+            throw new Error(`no codec found to encode value in union type ${name}`)
+          },
+      codecs
+    )
+  }
 }
 
 /**
@@ -1334,7 +1356,7 @@ export function tuple<CS extends [Mixed, ...Array<Mixed>]>(
       }
       return errors.length > 0 ? failures(errors) : success(as)
     },
-    useIdentity(codecs, len) ? identity : a => codecs.map((type, i) => type.encode(a[i])),
+    useIdentity(codecs) ? identity : a => codecs.map((type, i) => type.encode(a[i])),
     codecs
   )
 }
@@ -1437,194 +1459,9 @@ export const strict = <P extends Props>(props: P, name?: string): ExactC<TypeC<P
   return exact(type(props), name)
 }
 
-type IndexItem = [unknown, Any, Any]
-
-interface Index extends Array<IndexItem> {}
-
-interface IndexRecord extends Record<string, Index> {}
-
-/** @internal */
-export const emptyIndexRecord: IndexRecord = {}
-
-const monoidIndexRecord: Monoid<IndexRecord> = {
-  concat: (a, b) => {
-    if (a === monoidIndexRecord.empty) {
-      return b
-    }
-    if (b === monoidIndexRecord.empty) {
-      return a
-    }
-    const r = cloneIndexRecord(a)
-    for (const k in b) {
-      if (r.hasOwnProperty(k)) {
-        r[k].push(...b[k])
-      } else {
-        r[k] = b[k]
-      }
-    }
-    return r
-  },
-  empty: emptyIndexRecord
-}
-
-const isIndexRecordEmpty = (a: IndexRecord): boolean => {
-  for (const _ in a) {
-    return false
-  }
-  return true
-}
-
-const foldMapIndexRecord = <A>(as: Array<A>, f: (a: A) => IndexRecord): IndexRecord => {
-  return as.reduce((acc, a) => monoidIndexRecord.concat(acc, f(a)), monoidIndexRecord.empty)
-}
-
-const cloneIndexRecord = (a: IndexRecord): IndexRecord => {
-  const r: IndexRecord = {}
-  for (const k in a) {
-    r[k] = a[k].slice()
-  }
-  return r
-}
-
-const updateindexRecordOrigin = (origin: Any, indexRecord: IndexRecord): IndexRecord => {
-  const r: IndexRecord = {}
-  for (const k in indexRecord) {
-    r[k] = indexRecord[k].map<IndexItem>(([v, _, id]) => [v, origin, id])
-  }
-  return r
-}
-
-const getCodecIndexRecord = (codec: Any, origin: Any, id: Any): IndexRecord => {
-  if (isInterfaceCodec(codec) || isStrictCodec(codec)) {
-    const interfaceIndex: IndexRecord = {}
-    for (let k in codec.props) {
-      const prop = codec.props[k]
-      if (isLiteralCodec(prop)) {
-        const value = prop.value
-        interfaceIndex[k] = [[value, origin, id]]
-      }
-    }
-    return interfaceIndex
-  }
-  if (isIntersectionCodec(codec)) {
-    return foldMapIndexRecord(codec.types, type => getCodecIndexRecord(type, origin, codec))
-  }
-  if (isUnionCodec(codec)) {
-    return foldMapIndexRecord(codec.types, type => getCodecIndexRecord(type, origin, type))
-  }
-  if (isExactCodec(codec) || isRefinementCodec(codec)) {
-    return getCodecIndexRecord(codec.type, origin, codec)
-  }
-  if (isRecursiveCodec(codec)) {
-    const indexRecord = codec.getIndexRecord()
-    if (codec !== origin) {
-      return updateindexRecordOrigin(origin, indexRecord)
-    }
-    return indexRecord
-  }
-  return monoidIndexRecord.empty
-}
-
-let isRecursiveCodecIndexable = true
-
-const isIndexableCodec = (codec: Any): boolean => {
-  return (
-    ((isInterfaceCodec(codec) || isStrictCodec(codec)) &&
-      Object.keys(codec.props).some(key => isLiteralCodec(codec.props[key]))) ||
-    ((isExactCodec(codec) || isRefinementCodec(codec)) && isIndexableCodec(codec.type)) ||
-    (isIntersectionCodec(codec) && codec.types.some(isIndexableCodec)) ||
-    (isUnionCodec(codec) && codec.types.every(isIndexableCodec)) ||
-    (isRecursiveCodecIndexable && isRecursiveCodec(codec))
-  )
-}
-
-/**
- * @internal
- */
-export const getIndexRecord = (codecs: Array<Mixed>): IndexRecord => {
-  const len = codecs.length
-  if (len === 0 || !codecs.every(isIndexableCodec)) {
-    return monoidIndexRecord.empty
-  }
-  const firstCodec = codecs[0]
-  const ir: IndexRecord = cloneIndexRecord(getCodecIndexRecord(firstCodec, firstCodec, firstCodec))
-  for (let i = 1; i < len; i++) {
-    const codec = codecs[i]
-    const cir = getCodecIndexRecord(codec, codec, codec)
-    for (const k in ir) {
-      if (cir.hasOwnProperty(k)) {
-        const is = ir[k]
-        const cis = cir[k]
-        for (let j = 0; j < cis.length; j++) {
-          const indexItem = cis[j]
-          const index = is.findIndex(([v]) => v === indexItem[0])
-          if (index === -1) {
-            is.push(indexItem)
-          } else if (indexItem[2] !== is[index][2]) {
-            delete ir[k]
-            break
-          }
-        }
-      } else {
-        delete ir[k]
-      }
-    }
-  }
-  return isIndexRecordEmpty(ir) ? monoidIndexRecord.empty : ir
-}
-
-const getTaggedUnion = <Tag extends string, CS extends [Mixed, Mixed, ...Array<Mixed>]>(
-  index: Index,
-  tag: Tag,
-  codecs: CS,
-  name: string
-): TaggedUnionC<Tag, CS> => {
-  const len = codecs.length
-  const indexWithPosition: Array<[unknown, number]> = index.map<[unknown, number]>(([v, origin]) => [
-    v,
-    codecs.findIndex(codec => codec === origin)
-  ])
-  const findIndex = (tagValue: unknown): number | undefined => {
-    for (let i = 0; i < indexWithPosition.length; i++) {
-      const [value, position] = indexWithPosition[i]
-      if (value === tagValue) {
-        return position
-      }
-    }
-  }
-  const isTagValue = (u: unknown): u is LiteralValue => findIndex(u) !== undefined
-  return new TaggedUnionType(
-    name,
-    (u): u is TypeOf<CS[number]> => {
-      if (!UnknownRecord.is(u)) {
-        return false
-      }
-      const tagValue = u[tag]
-      const index = findIndex(tagValue)
-      return index !== undefined ? codecs[index].is(u) : false
-    },
-    (u, c) => {
-      const dictionaryResult = UnknownRecord.validate(u, c)
-      if (dictionaryResult.isLeft()) {
-        return dictionaryResult
-      }
-      const d = dictionaryResult.value
-      const tagValue = d[tag]
-      if (!isTagValue(tagValue)) {
-        return failure(u, c)
-      }
-      const index = findIndex(tagValue)!
-      const codec = codecs[index]
-      return codec.validate(d, appendContext(c, String(index), codec, d))
-    },
-    useIdentity(codecs, len) ? identity : a => codecs[findIndex(a[tag])!].encode(a),
-    codecs,
-    tag
-  )
-}
-
 /**
  * @since 1.3.0
+ * @deprecated
  */
 export class TaggedUnionType<
   Tag extends string,
@@ -1635,8 +1472,11 @@ export class TaggedUnionType<
 > extends UnionType<CS, A, O, I> {
   constructor(
     name: string,
+    // tslint:disable-next-line: deprecation
     is: TaggedUnionType<Tag, CS, A, O, I>['is'],
+    // tslint:disable-next-line: deprecation
     validate: TaggedUnionType<Tag, CS, A, O, I>['validate'],
+    // tslint:disable-next-line: deprecation
     encode: TaggedUnionType<Tag, CS, A, O, I>['encode'],
     codecs: CS,
     readonly tag: Tag
@@ -1647,27 +1487,32 @@ export class TaggedUnionType<
 
 /**
  * @since 1.5.3
+ * @deprecated
  */
-export interface TaggedUnionC<Tag extends string, CS extends [Mixed, Mixed, ...Array<Mixed>]>
+export interface TaggedUnionC<Tag extends string, CS extends [Mixed, Mixed, ...Array<Mixed>]>  // tslint:disable-next-line: deprecation
   extends TaggedUnionType<Tag, CS, TypeOf<CS[number]>, OutputOf<CS[number]>, unknown> {}
 
 /**
+ * Use `union` instead
+ *
  * @since 1.3.0
+ * @deprecated
  */
 export const taggedUnion = <Tag extends string, CS extends [Mixed, Mixed, ...Array<Mixed>]>(
   tag: Tag,
   codecs: CS,
   name: string = getUnionName(codecs)
+  // tslint:disable-next-line: deprecation
 ): TaggedUnionC<Tag, CS> => {
-  const indexRecord = getIndexRecord(codecs)
-  if (!indexRecord.hasOwnProperty(tag)) {
-    if (isRecursiveCodecIndexable && codecs.length > 0) {
-      console.warn(`[io-ts] Cannot build a tagged union for ${name}, returning a de-optimized union`)
-    }
-    const U = union(codecs, name)
+  const U = union(codecs, name)
+  // tslint:disable-next-line: deprecation
+  if (U instanceof TaggedUnionType) {
+    return U
+  } else {
+    console.warn(`[io-ts] Cannot build a tagged union for ${name}, returning a de-optimized union`)
+    // tslint:disable-next-line: deprecation
     return new TaggedUnionType(name, U.is, U.validate, U.encode, codecs, tag)
   }
-  return getTaggedUnion(indexRecord[tag], tag, codecs, name)
 }
 
 /**
@@ -2147,3 +1992,159 @@ export function alias<A, O, I>(
 <AA extends Exact<A, AA>, OO extends Exact<O, OO> = O>() => Type<AA, OO, I> {
   return () => codec as any
 }
+
+//
+// backporting
+//
+
+interface NonEmptyArray<A> extends Array<A> {
+  0: A
+}
+
+const isNonEmpty = <A>(as: Array<A>): as is NonEmptyArray<A> => as.length > 0
+
+interface Tags extends Record<string, NonEmptyArray<LiteralValue>> {}
+
+/**
+ * @internal
+ */
+export const emptyTags: Tags = {}
+
+function intersect(a: NonEmptyArray<LiteralValue>, b: NonEmptyArray<LiteralValue>): Array<LiteralValue> {
+  const r: Array<LiteralValue> = []
+  for (const v of a) {
+    if (b.indexOf(v) !== -1) {
+      r.push(v)
+    }
+  }
+  return r
+}
+
+function mergeTags(a: Tags, b: Tags): Tags {
+  if (a === emptyTags) {
+    return b
+  }
+  if (b === emptyTags) {
+    return a
+  }
+  let r: Tags = Object.assign({}, a)
+  for (const k in b) {
+    if (a.hasOwnProperty(k)) {
+      const intersection = intersect(a[k], b[k])
+      if (isNonEmpty(intersection)) {
+        r[k] = intersection
+      } else {
+        r = emptyTags
+        break
+      }
+    } else {
+      r[k] = b[k]
+    }
+  }
+  return r
+}
+
+function intersectTags(a: Tags, b: Tags): Tags {
+  if (a === emptyTags || b === emptyTags) {
+    return emptyTags
+  }
+  let r: Tags = emptyTags
+  for (const k in a) {
+    if (b.hasOwnProperty(k)) {
+      const intersection = intersect(a[k], b[k])
+      if (intersection.length === 0) {
+        if (r === emptyTags) {
+          r = {}
+        }
+        r[k] = a[k].concat(b[k]) as any
+      }
+    }
+  }
+  return r
+}
+
+function isLiteralC(codec: Any): codec is LiteralC<LiteralValue> {
+  return (codec as any)._tag === 'LiteralType'
+}
+
+function isTypeC(codec: Any): codec is TypeC<Props> {
+  return (codec as any)._tag === 'InterfaceType'
+}
+
+function isIntersectionC(codec: Any): codec is IntersectionC<[Mixed, Mixed, ...Array<Mixed>]> {
+  return (codec as any)._tag === 'IntersectionType'
+}
+
+function isUnionC(codec: Any): codec is UnionC<[Mixed, Mixed, ...Array<Mixed>]> {
+  return (codec as any)._tag === 'UnionType'
+}
+
+function isRecursiveC(codec: Any): codec is RecursiveType<Any> {
+  return (codec as any)._tag === 'RecursiveType'
+}
+
+let lazyCodec: Any | null = null
+
+/**
+ * @internal
+ */
+export function getTags(codec: Any): Tags {
+  if (codec === lazyCodec) {
+    return emptyTags
+  }
+  if (isTypeC(codec)) {
+    let index: Tags = emptyTags
+    // tslint:disable-next-line: forin
+    for (let k in codec.props) {
+      const prop = codec.props[k]
+      if (isLiteralC(prop)) {
+        if (index === emptyTags) {
+          index = {}
+        }
+        index[k] = [prop.value]
+      }
+    }
+    return index
+  } else if (isIntersectionC(codec)) {
+    return codec.types.reduce((tags, codec) => mergeTags(tags, getTags(codec)), emptyTags)
+  } else if (isUnionC(codec)) {
+    return codec.types.slice(1).reduce((tags, codec) => intersectTags(tags, getTags(codec)), getTags(codec.types[0]))
+  } else if (isRecursiveC(codec)) {
+    lazyCodec = codec
+    const tags = getTags(codec.type)
+    lazyCodec = null
+    return tags
+  }
+  return emptyTags
+}
+
+/**
+ * @internal
+ */
+export function getIndex(codecs: NonEmptyArray<Any>): [string, NonEmptyArray<NonEmptyArray<LiteralValue>>] | undefined {
+  const tags = getTags(codecs[0])
+  const keys = Object.keys(tags)
+  const len = codecs.length
+  keys: for (const k of keys) {
+    const all = tags[k].slice()
+    const index: NonEmptyArray<NonEmptyArray<LiteralValue>> = [tags[k]]
+    for (let i = 1; i < len; i++) {
+      const codec = codecs[i]
+      const ctags = getTags(codec)
+      const values = ctags[k]
+      // tslint:disable-next-line: strict-type-predicates
+      if (values === undefined) {
+        continue keys
+      } else {
+        if (values.some(v => all.indexOf(v) !== -1)) {
+          continue keys
+        } else {
+          all.push(...values)
+          index.push(values)
+        }
+      }
+    }
+    return [k, index]
+  }
+  return undefined
+}


Test patch:
diff --git a/test/getIndexRecord.ts b/test/getIndexRecord.ts
deleted file mode 100644
index fcc244b..0000000
--- a/test/getIndexRecord.ts
+++ /dev/null
@@ -1,286 +0,0 @@
-import * as assert from 'assert'
-import * as t from '../src/index'
-
-describe('getIndexRecord', () => {
-  const assertEmptyIndexRecord = (codecs: Array<t.Mixed>) => {
-    assert.strictEqual(t.getIndexRecord(codecs), t.emptyIndexRecord)
-  }
-
-  interface IndexRecord extends Record<string, Array<[unknown, t.Any, t.Any]>> {}
-
-  const assertEqualIndexRecord = (codecs: Array<t.Mixed>, expected: IndexRecord) => {
-    assert.deepStrictEqual(t.getIndexRecord(codecs), expected)
-  }
-
-  it('should handle zero types', () => {
-    assertEmptyIndexRecord([])
-  })
-
-  it('should handle non indexable types', () => {
-    assertEmptyIndexRecord([t.string])
-    assertEmptyIndexRecord([t.type({ a: t.string })])
-    assertEmptyIndexRecord([t.type({ a: t.string }), t.type({ b: t.number })])
-    assertEmptyIndexRecord([t.type({ type: t.literal('A') }), t.type({ b: t.number })])
-  })
-
-  it('should handle type codecs', () => {
-    const A = t.type({ type: t.literal('A') })
-    assertEqualIndexRecord([A], { type: [['A', A, A]] })
-    const B = t.type({ type: t.literal('B') })
-    assertEqualIndexRecord([A, B], { type: [['A', A, A], ['B', B, B]] })
-    const C = t.type({ kind: t.literal('A') })
-    assertEmptyIndexRecord([A, C])
-    const D = t.type({ type: t.literal('A') })
-    assertEmptyIndexRecord([A, D])
-  })
-
-  it('should handle refinement codecs', () => {
-    // tslint:disable-next-line: deprecation
-    const A = t.refinement(t.type({ type: t.literal('A'), a: t.string }), () => true)
-    assertEqualIndexRecord([A], { type: [['A', A, A]] })
-    // tslint:disable-next-line: deprecation
-    const B = t.refinement(t.type({ type: t.literal('B') }), () => true)
-    assertEqualIndexRecord([A, B], { type: [['A', A, A], ['B', B, B]] })
-    // tslint:disable-next-line: deprecation
-    const C = t.refinement(t.type({ kind: t.literal('A') }), () => true)
-    assertEmptyIndexRecord([A, C])
-    // tslint:disable-next-line: deprecation
-    const D = t.refinement(t.type({ type: t.literal('A') }), () => true)
-    assertEmptyIndexRecord([A, D])
-  })
-
-  it('should handle strict codecs', () => {
-    const A = t.strict({ type: t.literal('A') })
-    assertEqualIndexRecord([A], { type: [['A', A, A]] })
-    const B = t.strict({ type: t.literal('B') })
-    assertEqualIndexRecord([A, B], { type: [['A', A, A], ['B', B, B]] })
-    const C = t.strict({ kind: t.literal('A') })
-    assertEmptyIndexRecord([A, C])
-    const D = t.strict({ type: t.literal('A') })
-    assertEmptyIndexRecord([A, D])
-  })
-
-  it('should handle exact codecs', () => {
-    const A = t.exact(t.type({ type: t.literal('A') }))
-    assertEqualIndexRecord([A], { type: [['A', A, A]] })
-    const B = t.exact(t.type({ type: t.literal('B') }))
-    assertEqualIndexRecord([A, B], { type: [['A', A, A], ['B', B, B]] })
-    const C = t.exact(t.type({ kind: t.literal('A') }))
-    assertEmptyIndexRecord([A, C])
-    const D = t.exact(t.type({ type: t.literal('A') }))
-    assertEmptyIndexRecord([A, D])
-  })
-
-  it('should handle unions', () => {
-    const A = t.type({ type: t.literal('A') })
-    const B = t.type({ type: t.literal('B') })
-    const C = t.type({ type: t.literal('C') })
-
-    const AB = t.union([A, B])
-    assertEqualIndexRecord([AB], { type: [['A', AB, A], ['B', AB, B]] })
-    assertEqualIndexRecord([AB, C], { type: [['A', AB, A], ['B', AB, B], ['C', C, C]] })
-
-    const D = t.type({ kind: t.literal('C') })
-    assertEmptyIndexRecord([AB, D])
-
-    // #263
-    const E = t.union([t.type({ type: t.literal('A') }), t.type({ type: t.literal('B') })])
-    const F = t.type({ type: t.literal('B') })
-    assertEmptyIndexRecord([E, F])
-  })
-
-  it('should handle duplicated codecs', () => {
-    const A = t.type({ type: t.literal('A') })
-    assertEqualIndexRecord([A, A], { type: [['A', A, A]] })
-  })
-
-  it('should handle overlapping unions', () => {
-    const A = t.type({ type: t.literal('A') })
-    const B = t.type({ type: t.literal('B') })
-    const C = t.type({ type: t.literal('C') })
-
-    const AB = t.union([A, B])
-    const BC = t.union([B, C])
-    assertEqualIndexRecord([AB, BC], { type: [['A', AB, A], ['B', AB, B], ['C', BC, C]] })
-  })
-
-  it('should handle intersections', () => {
-    const A = t.intersection([t.partial({ a: t.string }), t.type({ type: t.literal('A') })])
-    assertEqualIndexRecord([A], { type: [['A', A, A]] })
-
-    const B = t.intersection([t.type({ type: t.literal('B1') }), t.type({ type: t.literal('B2') })])
-    assertEqualIndexRecord([B], { type: [['B1', B, B], ['B2', B, B]] })
-
-    const C = t.intersection([t.type({ type: t.literal('C') }), t.type({ type: t.literal('C') })])
-    assertEqualIndexRecord([C], { type: [['C', C, C], ['C', C, C]] })
-
-    const D = t.intersection([t.type({ type: t.literal('D1') }), t.type({ kind: t.literal('D2') })])
-    assertEqualIndexRecord([D], { type: [['D1', D, D]], kind: [['D2', D, D]] })
-
-    // #263
-    const E = t.intersection([t.type({ type: t.literal('A') }), t.type({ type: t.literal('B') })])
-    const F = t.type({ type: t.literal('B') })
-    assertEmptyIndexRecord([E, F])
-  })
-
-  it('should bail out when no tags are available', () => {
-    const A = t.type({ type: t.literal('A') })
-    const B = t.type({ kind: t.literal('B') })
-    assertEmptyIndexRecord([A, B])
-  })
-
-  it('should handle multiple tag candidates', () => {
-    const C = t.type({ type: t.literal('C1'), kind: t.literal('C2') })
-    const D = t.type({ type: t.literal('D1'), kind: t.literal('D2') })
-    assertEqualIndexRecord([C, D], {
-      type: [['C1', C, C], ['D1', D, D]],
-      kind: [['C2', C, C], ['D2', D, D]]
-    })
-  })
-
-  describe('recursive codecs', () => {
-    it('non tagged + union with non indexable codec', () => {
-      interface R {
-        ru: R | undefined
-        ur: undefined | R
-      }
-      const R: t.Type<R> = t.recursion('R', Self =>
-        t.type({
-          ru: t.union([Self, t.undefined]),
-          ur: t.union([t.undefined, Self])
-        })
-      )
-      assertEmptyIndexRecord([R])
-    })
-
-    it('tagged + union with non indexable codec', () => {
-      interface R {
-        type: 'R'
-        ru: R | undefined
-        ur: undefined | R
-      }
-      const R: t.Type<R> = t.recursion('R', Self =>
-        t.type({
-          type: t.literal('R'),
-          ru: t.union([Self, t.undefined]),
-          ur: t.union([t.undefined, Self])
-        })
-      )
-      assertEqualIndexRecord([R], { type: [['R', R, R]] })
-    })
-
-    it('non tagged + union with indexable codec', () => {
-      const A = t.type({
-        type: t.literal('A')
-      })
-      type A = t.TypeOf<typeof A>
-      interface R {
-        ra: R | A
-        ar: A | R
-      }
-      const R: t.Type<R> = t.recursion('R', Self =>
-        t.type({
-          ra: t.union([Self, A]),
-          ar: t.union([A, Self])
-        })
-      )
-      assertEmptyIndexRecord([R])
-      assertEmptyIndexRecord([A, R])
-    })
-
-    it('tagged + union with indexable codec', () => {
-      const A = t.type({
-        type: t.literal('A')
-      })
-      type A = t.TypeOf<typeof A>
-      interface R {
-        type: 'R'
-        ra: R | A
-        ar: A | R
-      }
-      const R: t.Type<R> = t.recursion('R', Self =>
-        t.type({
-          type: t.literal('R'),
-          ra: t.union([Self, A]),
-          ar: t.union([A, Self])
-        })
-      )
-      assertEqualIndexRecord([R], { type: [['R', R, R]] })
-      assertEqualIndexRecord([A, R], { type: [['A', A, A], ['R', R, R]] })
-    })
-
-    it('tagged + tagged union with indexable codec', () => {
-      const A = t.type({
-        type: t.literal('A')
-      })
-      type A = t.TypeOf<typeof A>
-      interface R {
-        type: 'R'
-        ar: A | R
-        ra: R | A
-      }
-      const R: t.Type<R> = t.recursion('R', () =>
-        t.type({
-          type: t.literal('R'),
-          ar: t.taggedUnion('type', [A, R]),
-          ra: t.taggedUnion('type', [R, A])
-        })
-      )
-      assertEqualIndexRecord([R], { type: [['R', R, R]] })
-      assertEqualIndexRecord([A, R], { type: [['A', A, A], ['R', R, R]] })
-    })
-
-    it('mutually recursive codecs', () => {
-      interface A {
-        type: 'A'
-        bu: B | undefined
-        ub: undefined | B
-      }
-      const A: t.Type<A> = t.recursion('A', () =>
-        t.type({
-          type: t.literal('A'),
-          bu: t.union([B, t.undefined]),
-          ub: t.union([t.undefined, B])
-        })
-      )
-      interface B {
-        type: 'B'
-        au: A | undefined
-        ua: undefined | A
-      }
-      const B: t.Type<B> = t.recursion('B', () =>
-        t.type({
-          type: t.literal('B'),
-          au: t.union([A, t.undefined]),
-          ua: t.union([t.undefined, A])
-        })
-      )
-      assertEqualIndexRecord([A], { type: [['A', A, A]] })
-      assertEqualIndexRecord([B], { type: [['B', B, B]] })
-      assertEqualIndexRecord([A, B], { type: [['A', A, A], ['B', B, B]] })
-    })
-  })
-
-  it('nested tagged union', () => {
-    const A = t.type({
-      type: t.literal('A')
-    })
-    type A = t.TypeOf<typeof A>
-    interface R {
-      type: 'R'
-      ra: R | A
-    }
-    const R: t.Type<R> = t.recursion('R', Self =>
-      t.type({
-        type: t.literal('R'),
-        ra: t.union([Self, A])
-      })
-    )
-    const AR = t.taggedUnion('type', [A, R])
-    const B = t.type({
-      type: t.literal('B')
-    })
-    const BAR = t.taggedUnion('type', [B, AR])
-    assertEqualIndexRecord(BAR.types, { type: [['B', B, B], ['A', AR, A], ['R', AR, R]] })
-  })
-})
diff --git a/test/taggedUnion.ts b/test/taggedUnion.ts
index 338f48a..ae5646d 100644
--- a/test/taggedUnion.ts
+++ b/test/taggedUnion.ts
@@ -2,12 +2,14 @@ import * as assert from 'assert'
 import * as t from '../src/index'
 import { assertFailure, assertStrictEqual, assertSuccess, NumberFromString } from './helpers'
 
+// tslint:disable-next-line: deprecation
 const OptionNumber = t.taggedUnion(
   'type',
   [t.type({ type: t.literal('None') }, 'None'), t.type({ type: t.literal('Some'), value: t.number }, 'Some')],
   'OptionNumber'
 )
 
+// tslint:disable-next-line: deprecation
 const OptionNumberFromString = t.taggedUnion(
   'type',
   [t.type({ type: t.literal('None') }, 'None'), t.type({ type: t.literal('Some'), value: NumberFromString }, 'Some')],
@@ -17,6 +19,7 @@ const OptionNumberFromString = t.taggedUnion(
 describe('taggedUnion', () => {
   describe('name', () => {
     it('should assign a default name', () => {
+      // tslint:disable-next-line: deprecation
       const OptionNumber = t.taggedUnion('type', [
         t.type({ type: t.literal('None') }),
         t.type({ type: t.literal('Some'), value: t.number })
@@ -25,6 +28,7 @@ describe('taggedUnion', () => {
     })
 
     it('should accept a name', () => {
+      // tslint:disable-next-line: deprecation
       const T = t.taggedUnion('type', OptionNumber.types, 'T')
       assert.strictEqual(T.name, 'T')
     })
@@ -56,6 +60,7 @@ describe('taggedUnion', () => {
     it('should handle intersections', () => {
       const A = t.intersection([t.type({ type: t.literal('A') }), t.partial({ a: t.string })], 'A')
       const B = t.type({ type: t.literal('B'), b: t.number }, 'B')
+      // tslint:disable-next-line: deprecation
       const T = t.taggedUnion('type', [A, B], 'T')
       assertSuccess(T.decode({ type: 'A' }))
       assertSuccess(T.decode({ type: 'B', b: 1 }))
@@ -74,6 +79,7 @@ describe('taggedUnion', () => {
           forest: t.array(Self)
         })
       )
+      // tslint:disable-next-line: deprecation
       const T = t.taggedUnion('type', [A, B], 'T')
       assertSuccess(T.decode({ type: 'A' }))
       assertSuccess(T.decode({ type: 'B', forest: [] }))
@@ -88,21 +94,21 @@ describe('taggedUnion', () => {
       const B = t.type({ type: t.literal('B'), b: t.string }, 'B')
       const C = t.type({ type: t.literal('C') }, 'C')
       const SubUnion = t.union([A, B], 'Subunion')
+      // tslint:disable-next-line: deprecation
       const T = t.taggedUnion('type', [SubUnion, C], 'T')
       assertSuccess(T.decode({ type: 'A' }))
       assertSuccess(T.decode({ type: 'B', b: 'b' }))
       assertSuccess(T.decode({ type: 'C' }))
-      assertFailure(T, { type: 'B' }, [
-        'Invalid value "B" supplied to : T/0: Subunion/0: A/type: "A"',
-        'Invalid value undefined supplied to : T/0: Subunion/1: B/b: string'
-      ])
+      assertFailure(T, { type: 'B' }, ['Invalid value undefined supplied to : T/0: Subunion/1: B/b: string'])
     })
 
     it('should handle sub tagged unions', () => {
       const A = t.type({ type: t.literal('A') })
       const B = t.type({ type: t.literal('B') })
       const C = t.type({ type: t.literal('C') })
+      // tslint:disable-next-line: deprecation
       const SubTaggedUnion = t.taggedUnion('type', [A, B])
+      // tslint:disable-next-line: deprecation
       const T = t.taggedUnion('type', [SubTaggedUnion, C])
       assertSuccess(T.decode({ type: 'A' }))
       assertSuccess(T.decode({ type: 'B' }))
@@ -115,6 +121,7 @@ describe('taggedUnion', () => {
     })
 
     it('should support numeric tags', () => {
+      // tslint:disable-next-line: deprecation
       const T = t.taggedUnion('type', [
         t.type({ type: t.literal(1), a: t.string }),
         t.type({ type: t.literal(2), b: t.number })
@@ -129,6 +136,7 @@ describe('taggedUnion', () => {
     })
 
     it('should support boolean tags', () => {
+      // tslint:disable-next-line: deprecation
       const T = t.taggedUnion('type', [
         t.type({ type: t.literal(true), a: t.string }),
         t.type({ type: t.literal(false), b: t.number })
@@ -143,6 +151,7 @@ describe('taggedUnion', () => {
     })
 
     it('should support mixed string, numeric and boolean tags', () => {
+      // tslint:disable-next-line: deprecation
       const T = t.taggedUnion(
         'type',
         [
@@ -183,41 +192,18 @@ describe('taggedUnion', () => {
     assert.strictEqual(T.encode, t.identity)
   })
 
-  describe('wrong arguments', () => {
-    it('should handle zero codecs', () => {
-      const T = t.taggedUnion('type', [] as any)
-      assertFailure(T, true, ['Invalid value true supplied to : ()'])
-    })
-
-    it('should handle one codec', () => {
-      const T = t.taggedUnion('type', [t.type({ type: t.literal('A') })] as any)
-      assertSuccess(T.decode({ type: 'A' }))
-      assertFailure(T, null, ['Invalid value null supplied to : ({ type: "A" })'])
-    })
-
-    it('should deoptimize if the arguments are wrong', () => {
-      const log: Array<string> = []
-      const warn = console.warn
-
-      console.warn = (message: string) => {
-        log.push(message)
-      }
-
-      const A = t.type({ type: t.literal('A'), a: t.number }, 'A')
-      // tslint:disable-next-line: deprecation
-      const B = t.refinement(A, x => x.a > 0, 'B')
-      t.taggedUnion('type', [B, A])
-
-      console.warn = warn
-
-      assert.deepStrictEqual(log, ['[io-ts] Cannot build a tagged union for (B | A), returning a de-optimized union'])
-    })
+  it('should handle one codec', () => {
+    // tslint:disable-next-line: deprecation
+    const T = t.taggedUnion('type', [t.type({ type: t.literal('A') })] as any)
+    assertSuccess(T.decode({ type: 'A' }))
+    assertFailure(T, null, ['Invalid value null supplied to : ({ type: "A" })'])
   })
 
   it('should warn for un-optimized unions', () => {
     const log: Array<string> = []
     const original = console.warn
     console.warn = (message: string) => log.push(message)
+    // tslint:disable-next-line: deprecation
     t.taggedUnion('type', [t.type({ type: t.literal('a') }), t.type({ bad: t.literal('b') })])
     console.warn = original
     assert.deepStrictEqual(log, [
diff --git a/test/union.ts b/test/union.ts
index eed4bf7..e1820f4 100644
--- a/test/union.ts
+++ b/test/union.ts
@@ -61,11 +61,6 @@ describe('union', () => {
     })
 
     describe('robustness', () => {
-      it('should handle zero codecs', () => {
-        const T = t.union([] as any)
-        assertFailure(T, true, ['Invalid value true supplied to : ()'])
-      })
-
       it('should handle one codec', () => {
         const T = t.union([t.string] as any)
         assertSuccess(T.decode('s'))
@@ -81,11 +76,6 @@ describe('union', () => {
       assert.strictEqual(T1.encode(1), 1)
     })
 
-    it('should encode a nullary union', () => {
-      const T0 = t.union([] as any)
-      assert.strictEqual(T0.encode(1 as never), 1)
-    })
-
     it('should return the same reference while encoding', () => {
       const T = t.union([t.type({ a: t.number }), t.string])
       assert.strictEqual(T.encode, t.identity)
@@ -127,25 +117,104 @@ describe('union', () => {
           )
         }
       }
+      const dateT = new DateT()
 
-      const U1 = t.union([new DateT(), t.null])
-      const u1 = 'not the right thing' as any
-      let failed = false
+      const U1 = t.union([dateT, t.null])
+      assert.throws(() => {
+        U1.encode(1 as any)
+      })
+      const M1 = t.type({ tag: t.literal('a'), a: dateT })
+      const M2 = t.type({ tag: t.literal('b'), b: t.number })
+      const T2 = t.union([M1, M2])
+      assert.throws(() => {
+        T2.encode(1 as any)
+      })
+    })
+  })
 
-      try {
-        U1.encode(u1)
-      } catch (e) {
-        failed = true
-      }
+  describe('getTags', () => {
+    it('not eligible', () => {
+      assert.strictEqual(t.getTags(t.string), t.emptyTags)
+    })
 
-      assert.equal(failed, true)
+    it('type', () => {
+      assert.strictEqual(t.getTags(t.type({ a: t.string })), t.emptyTags)
+      assert.deepStrictEqual(t.getTags(t.type({ tag: t.literal('a') })), { tag: ['a'] })
+      assert.deepStrictEqual(t.getTags(t.type({ tag: t.literal('a'), type: t.literal('b') })), {
+        tag: ['a'],
+        type: ['b']
+      })
+    })
+
+    it('intersection', () => {
+      assert.strictEqual(t.getTags(t.intersection([t.type({ a: t.string }), t.type({ b: t.number })])), t.emptyTags)
+      assert.deepStrictEqual(t.getTags(t.intersection([t.type({ a: t.literal('a') }), t.type({ b: t.number })])), {
+        a: ['a']
+      })
+      assert.deepStrictEqual(
+        t.getTags(t.intersection([t.type({ a: t.literal('a') }), t.type({ b: t.literal('b') })])),
+        {
+          a: ['a'],
+          b: ['b']
+        }
+      )
+      assert.deepStrictEqual(
+        t.getTags(t.intersection([t.type({ a: t.literal('a') }), t.type({ a: t.literal('a') })])),
+        {
+          a: ['a']
+        }
+      )
+      assert.strictEqual(
+        t.getTags(t.intersection([t.type({ a: t.literal('a') }), t.type({ a: t.literal('b') })])),
+        t.emptyTags
+      )
+    })
+
+    it('union', () => {
+      assert.strictEqual(t.getTags(t.union([t.string, t.number])), t.emptyTags)
+      assert.strictEqual(
+        t.getTags(t.union([t.type({ tag: t.literal('a') }), t.type({ type: t.literal('b') })])),
+        t.emptyTags
+      )
+      assert.strictEqual(
+        t.getTags(t.union([t.type({ tag: t.literal('a') }), t.type({ tag: t.literal('a') })])),
+        t.emptyTags
+      )
+      const U1 = t.union([t.type({ tag: t.literal('a') }), t.type({ tag: t.literal('b') })])
+      assert.deepStrictEqual(t.getTags(U1), {
+        tag: ['a', 'b']
+      })
+      const U2 = t.union([U1, t.type({ tag: t.literal('c') })])
+      assert.deepStrictEqual(t.getTags(U2), {
+        tag: ['a', 'b', 'c']
+      })
+      assert.deepStrictEqual(
+        t.getTags(
+          t.union([
+            t.type({ tag: t.literal('a'), type: t.literal('a') }),
+            t.type({ tag: t.literal('b'), type: t.literal('b') })
+          ])
+        ),
+        {
+          tag: ['a', 'b'],
+          type: ['a', 'b']
+        }
+      )
     })
   })
 
-  it.skip('should optimize tagged unions', () => {
-    const A = t.type({ type: t.literal('A') })
-    const B = t.type({ type: t.literal('B') })
-    const T = t.union([A, B])
-    assert.strictEqual(T instanceof t.TaggedUnionType, true)
+  it('getIndex', () => {
+    const M1 = t.type({ tag: t.literal('a') })
+    const M2 = t.type({ tag: t.literal('b') })
+    const M3 = t.intersection([t.type({ tag: t.literal('a') }), t.type({ b: t.number })])
+    const M4 = t.intersection([t.type({ tag: t.literal('c') }), t.type({ b: t.number })])
+    const U1 = t.union([M1, M2])
+    assert.deepStrictEqual(t.getIndex([t.string]), undefined)
+    assert.deepStrictEqual(t.getIndex([M1]), ['tag', [['a']]])
+    assert.deepStrictEqual(t.getIndex([M1, M1]), undefined)
+    assert.deepStrictEqual(t.getIndex([M1, M2]), ['tag', [['a'], ['b']]])
+    assert.deepStrictEqual(t.getIndex([M1, M2, M3]), undefined)
+    assert.deepStrictEqual(t.getIndex([M1, M2, M4]), ['tag', [['a'], ['b'], ['c']]])
+    assert.deepStrictEqual(t.getIndex([U1, M4]), ['tag', [['a', 'b'], ['c']]])
   })
 })