Skip to main content
Ben Nadel at Angular 2 Master Class (New York, NY) with: Torgeir Helgevold
Ben Nadel at Angular 2 Master Class (New York, NY) with: Torgeir Helgevold

Looking At Prototypal Inheritance To Determine Data Types In JavaScript

By
Published in Comments (7)

Yesterday, I was working on some JavaScript that needed to execute slightly different actions when a given value was either an Object or an Array. Typically for this, I would use something like Underscore.js or Lodash.js, which converts the value to a String and looks at the result; but, as I was writing the code, I wondered if we could use the given value's Prototype chain as a means to determine its true type.

In most of the type-checking methods that I see in JavaScript, a value will be converted to a string using the core toString() method provided by the Object prototype. And this works really well. But, as an experiment, I wanted to see if the same type check could be performed by augmenting the root Prototypes and then checking to see which value was inherited (via the prototype chain) by the given value.

NOTE: This is only a fun experiment.

<!doctype html>
<html>
<head>
	<title>
		Using Prototypal Inheritance To Test Data Type
	</title>

	<script type="text/javascript">


		// The usual approach to type checking is to convert the value
		// to a string and check the result against a known value.
		function usualIsArray( value ) {

			var toString = Object.prototype.toString;

			return( toString.call( value ) === "[object Array]" );

		}


		// The usual approach to type checking is to convert the value
		// to a string and check the result against a known value.
		function usualIsObject( value ) {

			var toString = Object.prototype.toString;

			return( toString.call( value ) === "[object Object]" );

		}


		// -------------------------------------------------- //
		// -------------------------------------------------- //


		// I check to see which core type references gets dynamically
		// inherited by the given value.
		function typeReference( value ) {

			// Quick check for null.
			if ( value === null ) {

				return( null );

			}

			// Quick check for undefined.
			var undefined = Object.prototype.undefined;

			if ( value === undefined ) {

				return( undefined );

			}

			// Temporarily inject root references.
			Object.prototype.__type_reference__ = Object;
			Array.prototype.__type_reference__ = Array;
			String.prototype.__type_reference__ = String;
			Number.prototype.__type_reference__ = Number;
			Boolean.prototype.__type_reference__ = Boolean;
			Date.prototype.__type_reference__ = Date;

			// Get prototypically inherited type reference that we
			// temporarily injected above.
			var reference = value.__type_reference__;

			// Clean up temp references.
			delete( Object.prototype.__type_reference__ );
			delete( Array.prototype.__type_reference__ );
			delete( String.prototype.__type_reference__ );
			delete( Number.prototype.__type_reference__ );
			delete( Boolean.prototype.__type_reference__ );
			delete( Date.prototype.__type_reference__ );

			return( reference );

		}


		// Checks data type by seeing if the given value inherits a
		// core prototype property.
		function isObject( value ) {

			return( typeReference( value ) === Object );

		}


		// Checks data type by seeing if the given value inherits a
		// core prototype property.
		function isArray( value ) {

			return( typeReference( value ) === Array );

		}


		// Checks data type by seeing if the given value inherits a
		// core prototype property.
		function isString( value ) {

			return( typeReference( value ) === String );

		}


		// Checks data type by seeing if the given value inherits a
		// core prototype property.
		function isNumber( value ) {

			return( typeReference( value ) === Number );

		}


		// Checks data type by seeing if the given value inherits a
		// core prototype property.
		function isBoolean( value ) {

			return( typeReference( value ) === Boolean );

		}


		// Checks data type by seeing if the given value inherits a
		// core prototype property.
		function isDate( value ) {

			return( typeReference( value ) === Date );

		}


		// Checks data type by seeing if the given value inherits a
		// core prototype property.
		function isNull( value ) {

			return( typeReference( value ) === null );

		}


		// Checks data type by seeing if the given value inherits a
		// core prototype property.
		function isUndefined( value ) {

			return( typeReference( value ) === window.undefined );

		}


		// -------------------------------------------------- //
		// -------------------------------------------------- //


		// Testing for an object.
		console.log( "- - isObject - -" );
		console.log( "Object:", isObject( {} ) );
		console.log( "Array:", isObject( [] ) );
		console.log( "String:", isObject( "test" ) );
		console.log( "Number:", isObject( 123 ) );
		console.log( "Boolean:", isObject( true ) );
		console.log( "Date:", isObject( new Date() ) );
		console.log( "Null:", isObject( null ) );
		console.log( "Undefined:", isObject( window.undefined ) );

		console.log( " " );

		// Testing for an array.
		console.log( "- - isArray - -" );
		console.log( "Object:", isArray( {} ) );
		console.log( "Array:", isArray( [] ) );
		console.log( "String:", isArray( "test" ) );
		console.log( "Number:", isArray( 123 ) );
		console.log( "Boolean:", isArray( true ) );
		console.log( "Date:", isArray( new Date() ) );
		console.log( "Null:", isArray( null ) );
		console.log( "Undefined:", isArray( window.undefined ) );

		console.log( " " );

		// Testing for a String.
		console.log( "- - isString - -" );
		console.log( "Object:", isString( {} ) );
		console.log( "Array:", isString( [] ) );
		console.log( "String:", isString( "test" ) );
		console.log( "Number:", isString( 123 ) );
		console.log( "Boolean:", isString( true ) );
		console.log( "Date:", isString( new Date() ) );
		console.log( "Null:", isString( null ) );
		console.log( "Undefined:", isString( window.undefined ) );

		console.log( " " );

		// Testing for a Number.
		console.log( "- - isNumber - -" );
		console.log( "Object:", isNumber( {} ) );
		console.log( "Array:", isNumber( [] ) );
		console.log( "String:", isNumber( "test" ) );
		console.log( "Number:", isNumber( 123 ) );
		console.log( "Boolean:", isNumber( true ) );
		console.log( "Date:", isNumber( new Date() ) );
		console.log( "Null:", isNumber( null ) );
		console.log( "Undefined:", isNumber( window.undefined ) );

		console.log( " " );

		// Testing for a Boolean.
		console.log( "- - isBoolean - -" );
		console.log( "Object:", isBoolean( {} ) );
		console.log( "Array:", isBoolean( [] ) );
		console.log( "String:", isBoolean( "test" ) );
		console.log( "Number:", isBoolean( 123 ) );
		console.log( "Boolean:", isBoolean( true ) );
		console.log( "Date:", isBoolean( new Date() ) );
		console.log( "Null:", isBoolean( null ) );
		console.log( "Undefined:", isBoolean( window.undefined ) );

		console.log( " " );

		// Testing for a Date.
		console.log( "- - isDate - -" );
		console.log( "Object:", isDate( {} ) );
		console.log( "Array:", isDate( [] ) );
		console.log( "String:", isDate( "test" ) );
		console.log( "Number:", isDate( 123 ) );
		console.log( "Boolean:", isDate( true ) );
		console.log( "Date:", isDate( new Date() ) );
		console.log( "Null:", isDate( null ) );
		console.log( "Undefined:", isDate( window.undefined ) );

		console.log( " " );

		// Testing for a Null.
		console.log( "- - isNull - -" );
		console.log( "Object:", isNull( {} ) );
		console.log( "Array:", isNull( [] ) );
		console.log( "String:", isNull( "test" ) );
		console.log( "Number:", isNull( 123 ) );
		console.log( "Boolean:", isNull( true ) );
		console.log( "Date:", isNull( new Date() ) );
		console.log( "Null:", isNull( null ) );
		console.log( "Undefined:", isNull( window.undefined ) );

		console.log( " " );

		// Testing for a Undefined.
		console.log( "- - isUndefined - -" );
		console.log( "Object:", isUndefined( {} ) );
		console.log( "Array:", isUndefined( [] ) );
		console.log( "String:", isUndefined( "test" ) );
		console.log( "Number:", isUndefined( 123 ) );
		console.log( "Boolean:", isUndefined( true ) );
		console.log( "Date:", isUndefined( new Date() ) );
		console.log( "Null:", isUndefined( null ) );
		console.log( "Undefined:", isUndefined( window.undefined ) );


		// -------------------------------------------------- //
		// -------------------------------------------------- //


		// Test how this behaves when sub-classing array.
		// NOTE: This not really how you would want to subclass an
		// array since the array has "magic" properties (ex. length).

		function MyArray() {}
		MyArray.prototype = Object.create( Array.prototype );

		// Create instance of new sub-classed array.
		var myArray = new MyArray();

		console.log( " " );
		console.log( "- - Sub-Classed Array - -" );
		console.log( "usualIsArray:", usualIsArray( myArray ) );
		console.log( "isArray:", isArray( myArray ) );


	</script>
</head>
<body>
	<!-- Left intentionally blank. -->
</body>
</html>

As you can see, I am injecting a circular JavaScript type reference into the Prototype of each JavaScript data type. Then, I am checking to see which type reference is inherited through prototypal inheritance. When I run the above code, I get the following console output:

    • isObject - -
      Object: true
      Array: false
      String: false
      Number: false
      Boolean: false
      Date: false
      Null: false
      Undefined: false
    • isArray - -
      Object: false
      Array: true
      String: false
      Number: false
      Boolean: false
      Date: false
      Null: false
      Undefined: false
    • isString - -
      Object: false
      Array: false
      String: true
      Number: false
      Boolean: false
      Date: false
      Null: false
      Undefined: false
    • isNumber - -
      Object: false
      Array: false
      String: false
      Number: true
      Boolean: false
      Date: false
      Null: false
      Undefined: false
    • isBoolean - -
      Object: false
      Array: false
      String: false
      Number: false
      Boolean: true
      Date: false
      Null: false
      Undefined: false
    • isDate - -
      Object: false
      Array: false
      String: false
      Number: false
      Boolean: false
      Date: true
      Null: false
      Undefined: false
    • isNull - -
      Object: false
      Array: false
      String: false
      Number: false
      Boolean: false
      Date: false
      Null: true
      Undefined: false
    • isUndefined - -
      Object: false
      Array: false
      String: false
      Number: false
      Boolean: false
      Date: false
      Null: false
      Undefined: true
    • Sub-Classed Array - -
      usualIsArray: false
      isArray: true

As you can see, I was able to determine each data type based on the reference provided by the Prototype chain. And, what's more, this method also correctly identifies a sub-classed Array as an Array, rather than as an Object (which is the incorrect result the toString() approach will provide).

I'm not actually advocating this approach; the Object.prototype.toString() approach is much faster (I assume) and requires significantly less code. I only found this experiment interesting because it leverages the existing prototypal inheritance mechanism rather than piggy-backing on some coincidental behavior exhibited by the toString() method.

Want to use code from this post? Check out the license.

Reader Comments

29 Comments

I remember seeing something like this in SmallTalk, but it went far and above what you just described here. In the little that I learned of SmallTalk, it seemed typical that every class would define a function identifying itself (and any descendants) as an instance of that class.

It would be simple to implement this in JavaScript. For instance, I would create a class Ball and one of its methods would be isBall(). It would also define isBall() as a method in Object. Object.isBall() would return false and Ball.isBall() would return true. If you defined descendants of Ball, Basketball, Baseball, and SoccerBall, the isBall() method would return true for them as well.

18 Comments

@Ben I like this. You can get a significant speed boost in real usage by removing the typeReference function and having the add/remove property stuff inside the isBlah functions.

e.g.

function isNumber(value){

/* null and undefined checks go here if desired */

Number.prototype.__type_reference__ = Number;
var reference = value.__type_reference__;
delete (Number.prototype.__type_reference__);

return (reference === Number);
}

15,848 Comments

@Grumpy,

Good call; and good point that I don't necessarily need to be setting ALL the reference types for each type-check. As long as I set / check the single reference type, it will work.

@Paul,

Are you saying that the base object would contain isXYZ methods for all derived types?

29 Comments

@Ben,
Yes, that would be the result. In JavaScript, you would end up with an Object class that has isBall, isBasketball, isBaseball, isSoccerBall, etc.

The trade-off is having a function defined that will always return a boolean value at the cost of having all of those functions defined for every class.

15,848 Comments

@Aladdin,

Ha ha, I wish!!! Work has just been eating up my free time lately :( I actually just got one AngularJS post up today; but it's a quick one. I keep waiting for some more free time, but there's not enough. I have some interesting ideas in the queue, though, so when I have time, expect more!

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel