feature: Upgrade TypeMatcher, deprecate isInstanceOf (#88)

`TypeMatcher`
- No longer abstract
- Added type parameter
- Deprecate the existing `name` parameter, tell folks to the type param
- Added `having` method which allows chained validation of features
- Eliminated 13 private implementations from the package
  - Just use it directly.
- Moved to its own file

Deprecate `isInstanceOf` class.
- Tell folks to use `TypeMatcher<T>` instead
- Run away from weirdly named classes

Tests
- centralizing tests in type_matcher_test
- Removed isInstanceOf tests from core_matchers_test
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2c7d11e..2f238d5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,25 @@
+## 0.12.3
+
+- Many improvements to `TypeMatcher`
+  - Can now be used directly as `const TypeMatcher<MyType>()`.
+  - Added a type parameter to specify the target `Type`. 
+    - Made the `name` constructor parameter optional and marked it deprecated.
+      It's redundant to the type parameter.
+  - Migrated all `isType` matchers to `TypeMatcher`.
+  - Added a `having` function that allows chained validations of specific
+    features of the target type.
+
+    ```dart
+    /// Validates that the object is a [RangeError] with a message containing
+    /// the string 'details' and `start` and `end` properties that are `null`.
+    final _rangeMatcher = isRangeError
+       .having((e) => e.message, 'message', contains('details'))
+       .having((e) => e.start, 'start', isNull)
+       .having((e) => e.end, 'end', isNull);
+    ```
+
+- Deprecated the `isInstanceOf` class. Use `TypeMatcher` instead.
+
 ## 0.12.2+1
 
 - Updated SDK version to 2.0.0-dev.17.0
diff --git a/lib/matcher.dart b/lib/matcher.dart
index d532926..72918aa 100644
--- a/lib/matcher.dart
+++ b/lib/matcher.dart
@@ -15,4 +15,5 @@
 export 'src/operator_matchers.dart';
 export 'src/order_matchers.dart';
 export 'src/string_matchers.dart';
+export 'src/type_matcher.dart';
 export 'src/util.dart';
diff --git a/lib/src/core_matchers.dart b/lib/src/core_matchers.dart
index 681f206..1de4b8d 100644
--- a/lib/src/core_matchers.dart
+++ b/lib/src/core_matchers.dart
@@ -3,6 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 
 import 'interfaces.dart';
+import 'type_matcher.dart';
 import 'util.dart';
 
 /// Returns a matcher that matches the isEmpty property.
@@ -103,24 +104,14 @@
   Description describe(Description description) => description.add('anything');
 }
 
+/// **DEPRECATED** Use [TypeMatcher] instead.
+///
 /// Returns a matcher that matches if an object is an instance
 /// of [T] (or a subtype).
-///
-/// As types are not first class objects in Dart we can only
-/// approximate this test by using a generic wrapper class.
-///
-/// For example, to test whether 'bar' is an instance of type
-/// 'Foo', we would write:
-///
-///     expect(bar, new isInstanceOf<Foo>());
+@Deprecated('Use `const TypeMatcher<MyType>()` instead.')
 // ignore: camel_case_types
-class isInstanceOf<T> extends Matcher {
+class isInstanceOf<T> extends TypeMatcher<T> {
   const isInstanceOf();
-
-  bool matches(item, Map matchState) => item is T;
-
-  Description describe(Description description) =>
-      description.add('an instance of $T');
 }
 
 /// A matcher that matches a function call against no exception.
@@ -157,48 +148,11 @@
   }
 }
 
-/*
- * Matchers for different exception types. Ideally we should just be able to
- * use something like:
- *
- * final Matcher throwsException =
- *     const _Throws(const isInstanceOf<Exception>());
- *
- * Unfortunately instanceOf is not working with dart2js.
- *
- * Alternatively, if static functions could be used in const expressions,
- * we could use:
- *
- * bool _isException(x) => x is Exception;
- * final Matcher isException = const _Predicate(_isException, "Exception");
- * final Matcher throwsException = const _Throws(isException);
- *
- * But currently using static functions in const expressions is not supported.
- * For now the only solution for all platforms seems to be separate classes
- * for each exception type.
- */
+/// A matcher for [Map].
+const isMap = const TypeMatcher<Map>();
 
-abstract class TypeMatcher extends Matcher {
-  final String _name;
-  const TypeMatcher(this._name);
-  Description describe(Description description) => description.add(_name);
-}
-
-/// A matcher for Map types.
-const Matcher isMap = const _IsMap();
-
-class _IsMap extends TypeMatcher {
-  const _IsMap() : super("Map");
-  bool matches(item, Map matchState) => item is Map;
-}
-
-/// A matcher for List types.
-const Matcher isList = const _IsList();
-
-class _IsList extends TypeMatcher {
-  const _IsList() : super('List');
-  bool matches(item, Map matchState) => item is List;
-}
+/// A matcher for [List].
+const isList = const TypeMatcher<List>();
 
 /// Returns a matcher that matches if an object has a length property
 /// that matches [matcher].
diff --git a/lib/src/error_matchers.dart b/lib/src/error_matchers.dart
index 1f37538..eb185f4 100644
--- a/lib/src/error_matchers.dart
+++ b/lib/src/error_matchers.dart
@@ -2,94 +2,39 @@
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
 
-import 'core_matchers.dart';
-import 'interfaces.dart';
+import 'type_matcher.dart';
 
-/// A matcher for ArgumentErrors.
-const Matcher isArgumentError = const _ArgumentError();
+/// A matcher for [ArgumentError].
+const isArgumentError = const TypeMatcher<ArgumentError>();
 
-class _ArgumentError extends TypeMatcher {
-  const _ArgumentError() : super("ArgumentError");
-  bool matches(item, Map matchState) => item is ArgumentError;
-}
+/// A matcher for [ConcurrentModificationError].
+const isConcurrentModificationError =
+    const TypeMatcher<ConcurrentModificationError>();
 
-/// A matcher for ConcurrentModificationError.
-const Matcher isConcurrentModificationError =
-    const _ConcurrentModificationError();
+/// A matcher for [CyclicInitializationError].
+const isCyclicInitializationError =
+    const TypeMatcher<CyclicInitializationError>();
 
-class _ConcurrentModificationError extends TypeMatcher {
-  const _ConcurrentModificationError() : super("ConcurrentModificationError");
-  bool matches(item, Map matchState) => item is ConcurrentModificationError;
-}
+/// A matcher for [Exception].
+const isException = const TypeMatcher<Exception>();
 
-/// A matcher for CyclicInitializationError.
-const Matcher isCyclicInitializationError = const _CyclicInitializationError();
+/// A matcher for [FormatException].
+const isFormatException = const TypeMatcher<FormatException>();
 
-class _CyclicInitializationError extends TypeMatcher {
-  const _CyclicInitializationError() : super("CyclicInitializationError");
-  bool matches(item, Map matchState) => item is CyclicInitializationError;
-}
+/// A matcher for [NoSuchMethodError].
+const isNoSuchMethodError = const TypeMatcher<NoSuchMethodError>();
 
-/// A matcher for Exceptions.
-const Matcher isException = const _Exception();
+/// A matcher for [NullThrownError].
+const isNullThrownError = const TypeMatcher<NullThrownError>();
 
-class _Exception extends TypeMatcher {
-  const _Exception() : super("Exception");
-  bool matches(item, Map matchState) => item is Exception;
-}
+/// A matcher for [RangeError].
+const isRangeError = const TypeMatcher<RangeError>();
 
-/// A matcher for FormatExceptions.
-const Matcher isFormatException = const _FormatException();
+/// A matcher for [StateError].
+const isStateError = const TypeMatcher<StateError>();
 
-class _FormatException extends TypeMatcher {
-  const _FormatException() : super("FormatException");
-  bool matches(item, Map matchState) => item is FormatException;
-}
+/// A matcher for [UnimplementedError].
+const isUnimplementedError = const TypeMatcher<UnimplementedError>();
 
-/// A matcher for NoSuchMethodErrors.
-const Matcher isNoSuchMethodError = const _NoSuchMethodError();
-
-class _NoSuchMethodError extends TypeMatcher {
-  const _NoSuchMethodError() : super("NoSuchMethodError");
-  bool matches(item, Map matchState) => item is NoSuchMethodError;
-}
-
-/// A matcher for NullThrownError.
-const Matcher isNullThrownError = const _NullThrownError();
-
-class _NullThrownError extends TypeMatcher {
-  const _NullThrownError() : super("NullThrownError");
-  bool matches(item, Map matchState) => item is NullThrownError;
-}
-
-/// A matcher for RangeErrors.
-const Matcher isRangeError = const _RangeError();
-
-class _RangeError extends TypeMatcher {
-  const _RangeError() : super("RangeError");
-  bool matches(item, Map matchState) => item is RangeError;
-}
-
-/// A matcher for StateErrors.
-const Matcher isStateError = const _StateError();
-
-class _StateError extends TypeMatcher {
-  const _StateError() : super("StateError");
-  bool matches(item, Map matchState) => item is StateError;
-}
-
-/// A matcher for UnimplementedErrors.
-const Matcher isUnimplementedError = const _UnimplementedError();
-
-class _UnimplementedError extends TypeMatcher {
-  const _UnimplementedError() : super("UnimplementedError");
-  bool matches(item, Map matchState) => item is UnimplementedError;
-}
-
-/// A matcher for UnsupportedError.
-const Matcher isUnsupportedError = const _UnsupportedError();
-
-class _UnsupportedError extends TypeMatcher {
-  const _UnsupportedError() : super("UnsupportedError");
-  bool matches(item, Map matchState) => item is UnsupportedError;
-}
+/// A matcher for [UnsupportedError].
+const isUnsupportedError = const TypeMatcher<UnsupportedError>();
diff --git a/lib/src/having_matcher.dart b/lib/src/having_matcher.dart
new file mode 100644
index 0000000..1684a93
--- /dev/null
+++ b/lib/src/having_matcher.dart
@@ -0,0 +1,62 @@
+// Copyright (c) 2018, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'custom_matcher.dart';
+import 'interfaces.dart';
+import 'type_matcher.dart';
+import 'util.dart';
+
+/// A package-private [TypeMatcher] implementation that handles is returned
+/// by calls to [TypeMatcher.having].
+class HavingMatcher<T> implements TypeMatcher<T> {
+  final TypeMatcher<T> _parent;
+  final List<_FunctionMatcher> _functionMatchers;
+
+  HavingMatcher(TypeMatcher<T> parent, String description,
+      Object feature(T source), Object matcher,
+      [Iterable<_FunctionMatcher> existing])
+      : this._parent = parent,
+        this._functionMatchers = <_FunctionMatcher>[]
+          ..addAll(existing ?? [])
+          ..add(new _FunctionMatcher<T>(description, feature, matcher));
+
+  TypeMatcher<T> having(
+          Object feature(T source), String description, Object matcher) =>
+      new HavingMatcher(
+          _parent, description, feature, matcher, _functionMatchers);
+
+  bool matches(item, Map matchState) {
+    for (var matcher in <Matcher>[_parent].followedBy(_functionMatchers)) {
+      if (!matcher.matches(item, matchState)) {
+        addStateInfo(matchState, {'matcher': matcher});
+        return false;
+      }
+    }
+    return true;
+  }
+
+  Description describeMismatch(
+      item, Description mismatchDescription, Map matchState, bool verbose) {
+    var matcher = matchState['matcher'] as Matcher;
+    matcher.describeMismatch(
+        item, mismatchDescription, matchState['state'] as Map, verbose);
+    return mismatchDescription;
+  }
+
+  Description describe(Description description) => description
+      .add('')
+      .addDescriptionOf(_parent)
+      .add(' with ')
+      .addAll('', ' and ', '', _functionMatchers);
+}
+
+class _FunctionMatcher<T> extends CustomMatcher {
+  final dynamic Function(T value) _feature;
+
+  _FunctionMatcher(String name, this._feature, matcher)
+      : super('`$name`:', '`$name`', matcher);
+
+  @override
+  Object featureValueOf(covariant T actual) => _feature(actual);
+}
diff --git a/lib/src/type_matcher.dart b/lib/src/type_matcher.dart
new file mode 100644
index 0000000..91552c9
--- /dev/null
+++ b/lib/src/type_matcher.dart
@@ -0,0 +1,89 @@
+// Copyright (c) 2018, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'having_matcher.dart';
+import 'interfaces.dart';
+
+/// A [Matcher] subclass that supports validating the [Type] of the target
+/// object.
+///
+/// ```dart
+/// expect(shouldBeDuration, new TypeMatcher<Duration>());
+/// ```
+///
+/// If you want to further validate attributes of the specified [Type], use the
+/// [having] function.
+///
+/// ```dart
+/// void shouldThrowRangeError(int value) {
+///   throw new RangeError.range(value, 10, 20);
+/// }
+///
+/// expect(
+///     () => shouldThrowRangeError(5),
+///     throwsA(const TypeMatcher<RangeError>()
+///         .having((e) => e.start, 'start', greaterThanOrEqualTo(10))
+///         .having((e) => e.end, 'end', lessThanOrEqualTo(20))));
+/// ```
+///
+/// Notice that you can chain multiple calls to [having] to verify multiple
+/// aspects of an object.
+///
+/// Note: All of the top-level `isType` matchers exposed by this package are
+/// instances of [TypeMatcher], so you can use the [having] function without
+/// creating your own instance.
+///
+/// ```dart
+/// expect(
+///     () => shouldThrowRangeError(5),
+///     throwsA(isRangeError
+///         .having((e) => e.start, 'start', greaterThanOrEqualTo(10))
+///         .having((e) => e.end, 'end', lessThanOrEqualTo(20))));
+/// ```
+class TypeMatcher<T> extends Matcher {
+  final String _name;
+  const TypeMatcher(
+      [@Deprecated('Provide a type argument to TypeMatcher and omit the name. '
+          'This argument will be removed in the next release.')
+          String name])
+      : this._name =
+            // ignore: deprecated_member_use
+            name;
+
+  /// Returns a new [TypeMatcher] that validates the existing type as well as
+  /// a specific [feature] of the object with the provided [matcher].
+  ///
+  /// Provides a human-readable [description] of the [feature] to make debugging
+  /// failures easier.
+  ///
+  /// ```dart
+  /// /// Validates that the object is a [RangeError] with a message containing
+  /// /// the string 'details' and `start` and `end` properties that are `null`.
+  /// final _rangeMatcher = isRangeError
+  ///    .having((e) => e.message, 'message', contains('details'))
+  ///    .having((e) => e.start, 'start', isNull)
+  ///    .having((e) => e.end, 'end', isNull);
+  /// ```
+  TypeMatcher<T> having(
+          Object feature(T source), String description, Object matcher) =>
+      new HavingMatcher(this, description, feature, matcher);
+
+  Description describe(Description description) {
+    var name = _name ?? _stripDynamic(T);
+    return description.add("<Instance of '$name'>");
+  }
+
+  bool matches(Object item, Map matchState) => item is T;
+}
+
+final _dart2DynamicArgs = new RegExp('<dynamic(, dynamic)*>');
+
+/// With this expression `{}.runtimeType.toString`,
+/// Dart 1: "<Instance of Map>
+/// Dart 2: "<Instance of Map<dynamic, dynamic>>"
+///
+/// This functions returns the Dart 1 output, when Dart 2 runtime semantics
+/// are enabled.
+String _stripDynamic(Type type) =>
+    type.toString().replaceAll(_dart2DynamicArgs, '');
diff --git a/test/core_matchers_test.dart b/test/core_matchers_test.dart
index b5e75b1..48e7672 100644
--- a/test/core_matchers_test.dart
+++ b/test/core_matchers_test.dart
@@ -228,12 +228,6 @@
     shouldFail(actual3, equals(expected3), reason3);
   });
 
-  test('isInstanceOf', () {
-    shouldFail(0, const isInstanceOf<String>(),
-        "Expected: an instance of String Actual: <0>");
-    shouldPass('cow', const isInstanceOf<String>());
-  });
-
   group('Predicate Matchers', () {
     test('isInstanceOf', () {
       shouldFail(0, predicate((x) => x is String, "an instance of String"),
diff --git a/test/having_test.dart b/test/having_test.dart
new file mode 100644
index 0000000..31cf7ac
--- /dev/null
+++ b/test/having_test.dart
@@ -0,0 +1,91 @@
+// Copyright (c) 2018, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:matcher/matcher.dart';
+import 'package:test/test.dart' show test, expect, throwsA, group;
+
+import 'test_utils.dart';
+
+void main() {
+  test('success', () {
+    shouldPass(new RangeError('details'), _rangeMatcher);
+  });
+
+  test('failure', () {
+    shouldFail(
+        new RangeError.range(-1, 1, 10),
+        _rangeMatcher,
+        "Expected: <Instance of 'RangeError'> with "
+        "`message`: contains 'details' and `start`: null and `end`: null "
+        'Actual: RangeError:<RangeError: '
+        'Invalid value: Not in range 1..10, inclusive: -1> '
+        "Which: has `message` with value 'Invalid value'");
+  });
+
+  // This code is used in the [TypeMatcher] doc comments.
+  test('integaration and example', () {
+    void shouldThrowRangeError(int value) {
+      throw new RangeError.range(value, 10, 20);
+    }
+
+    expect(
+        () => shouldThrowRangeError(5),
+        throwsA(const TypeMatcher<RangeError>()
+            .having((e) => e.start, 'start', greaterThanOrEqualTo(10))
+            .having((e) => e.end, 'end', lessThanOrEqualTo(20))));
+
+    expect(
+        () => shouldThrowRangeError(5),
+        throwsA(isRangeError
+            .having((e) => e.start, 'start', greaterThanOrEqualTo(10))
+            .having((e) => e.end, 'end', lessThanOrEqualTo(20))));
+  });
+
+  group('CustomMater copy', () {
+    test("Feature Matcher", () {
+      var w = new Widget();
+      w.price = 10;
+      shouldPass(w, _hasPrice(10));
+      shouldPass(w, _hasPrice(greaterThan(0)));
+      shouldFail(
+          w,
+          _hasPrice(greaterThan(10)),
+          "Expected: <Instance of 'Widget'> with `price`: a value greater than <10> "
+          "Actual: <Instance of 'Widget'> "
+          "Which: has `price` with value <10> which is not "
+          "a value greater than <10>");
+    });
+
+    test("Custom Matcher Exception", () {
+      shouldFail(
+          'a',
+          _badCustomMatcher(),
+          allOf([
+            contains(
+                "Expected: <Instance of 'Widget'> with `feature`: {1: 'a'} "),
+            contains("Actual: 'a'"),
+          ]));
+      shouldFail(
+          new Widget(),
+          _badCustomMatcher(),
+          allOf([
+            contains(
+                "Expected: <Instance of 'Widget'> with `feature`: {1: 'a'} "),
+            contains("Actual: <Instance of 'Widget'> "),
+            contains("Which: threw 'Exception: bang' "),
+          ]));
+    });
+  });
+}
+
+final _rangeMatcher = isRangeError
+    .having((e) => e.message, 'message', contains('details'))
+    .having((e) => e.start, 'start', isNull)
+    .having((e) => e.end, 'end', isNull);
+
+Matcher _hasPrice(matcher) =>
+    const TypeMatcher<Widget>().having((e) => e.price, 'price', matcher);
+
+Matcher _badCustomMatcher() => const TypeMatcher<Widget>()
+    .having((e) => throw new Exception("bang"), 'feature', {1: "a"});
diff --git a/test/type_matcher_test.dart b/test/type_matcher_test.dart
index c15562b..e20294f 100644
--- a/test/type_matcher_test.dart
+++ b/test/type_matcher_test.dart
@@ -26,8 +26,10 @@
   _test('NullThrownError', isNullThrownError, new NullThrownError());
 
   group('custom `TypeMatcher`', () {
+    // ignore: deprecated_member_use
     _test('String', const isInstanceOf<String>(), 'hello');
     _test('String', const _StringMatcher(), 'hello');
+    _test('String', const TypeMatcher<String>(), 'hello');
   });
 }
 
@@ -41,27 +43,21 @@
     }
 
     test('fails', () {
-      shouldFail(
-          const _TestType(),
-          typeMatcher,
-          anyOf(
-              // Handles the TypeMatcher case
-              equalsIgnoringWhitespace('Expected: $name Actual: ?:<TestType>'),
-              // Handles the `isInstanceOf` case
-              equalsIgnoringWhitespace(
-                  'Expected: an instance of $name Actual: ?:<TestType>')));
+      shouldFail(const TestType(), typeMatcher,
+          "Expected: <Instance of '$name'> Actual: <Instance of 'TestType'>");
     });
   });
 }
 
+// Validate that existing implementations continue to work.
 class _StringMatcher extends TypeMatcher {
-  const _StringMatcher() : super('String');
+  const _StringMatcher() : super(
+            // ignore: deprecated_member_use
+            'String');
 
   bool matches(item, Map matchState) => item is String;
 }
 
-class _TestType {
-  const _TestType();
-
-  String toString() => 'TestType';
+class TestType {
+  const TestType();
 }