JavaScript's typeof operator is the standard way to ask what kind of value you're holding — but it has well-known holes. typeof null is "object". typeof [] is "object". typeof NaN is "number". You'll build a small library of type-check helpers that paper over those holes so callers get the answer they actually expected.
Implement and export nine helpers from a single module. Each one takes a value and returns a boolean. The full set: isString, isNumber, isBoolean, isArray, isObject, isFunction, isNull, isUndefined, and isNaN. isObject returns true only for plain objects — not arrays, not null. isNumber returns true only for real numbers — NaN does not count as a number here. Use Number.isNaN for the NaN check; the global isNaN coerces its argument first and is not what you want.
function isString(value) { /* returns boolean */ }
function isNumber(value) { /* returns boolean — NaN is NOT a number */ }
function isBoolean(value) { /* returns boolean */ }
function isArray(value) { /* returns boolean */ }
function isObject(value) { /* returns boolean — null and arrays are NOT objects */ }
function isFunction(value) { /* returns boolean */ }
function isNull(value) { /* returns boolean — only for the value null */ }
function isUndefined(value) { /* returns boolean — only for the value undefined */ }
function isNaN(value) { /* returns boolean — only for the NaN value, no coercion */ }
module.exports = {
isString, isNumber, isBoolean, isArray, isObject,
isFunction, isNull, isUndefined, isNaN,
};
isString('hello'); // true
isString(''); // true — empty string is still a string
isString(new String('hi')); // false — boxed wrapper, not a primitive string
isNumber(42); // true
isNumber(NaN); // false — NaN is excluded from "number" here
isNumber('3'); // false — strings that look like numbers don't count
isObject({}); // true
isObject({ a: 1 }); // true
isObject(null); // false — typeof says 'object' but null is its own thing
isObject([1, 2]); // false — arrays get their own helper
isArray([]); // true
isArray([1, 2, 3]); // true
isArray('abc'); // false — strings are iterable, not arrays
isNull(null); // true
isNull(undefined); // false
isUndefined(undefined); // true
isUndefined(null); // false
isNaN(NaN); // true
isNaN('foo'); // false — no coercion (global isNaN would say true)
isNaN(42); // false
typeof for primitives — 'string', 'number', 'boolean', 'function', 'undefined' are all reliably reported by typeof.Array.isArray for arrays — typeof [] is 'object', so typeof alone can't distinguish arrays from plain objects.isObject against null and arrays — typeof null === 'object' is a historical JavaScript bug; you must explicitly reject null and arrays.Number.isNaN, not the global isNaN — global isNaN('foo') returns true because it coerces first. Number.isNaN only returns true for the literal NaN value.Date, Map, Set, class instances all pass isObject here. Distinguishing them is out of scope for v1.You'll write nine small helpers that mostly delegate to typeof, with three carefully-placed extra checks that paper over the language's three historical quirks: typeof null === 'object', typeof [] === 'object', and typeof NaN === 'number'.
JavaScript has one built-in type-asking operator — typeof — and it's mostly fine. typeof 'hi' is 'string'. typeof 42 is 'number'. typeof true is 'boolean'. But it has three lies baked into the language for backwards-compatibility reasons that nobody can fix at this point. Your job is to write a small set of helpers — isString, isNumber, isBoolean, isArray, isObject, isFunction, isNull, isUndefined, isNaN — that callers can trust without remembering the quirks.
typeof returns one of seven strings: 'string', 'number', 'boolean', 'undefined', 'object', 'function', 'symbol', 'bigint'. Most of those map 1:1 to the helper you want. The trouble is the 'object' and 'number' buckets: each one contains values that shouldn't really live there.
For the honest rows you can write a one-liner. For the three lying rows you need a second check to filter the lie back out.
A reasonable first try is to map each helper directly to typeof:
function isString(value) { return typeof value === 'string'; }
function isNumber(value) { return typeof value === 'number'; } // BUG: NaN slips through
function isBoolean(value) { return typeof value === 'boolean'; }
function isArray(value) { return typeof value === 'object'; } // BUG: {} and null slip through
function isObject(value) { return typeof value === 'object'; } // BUG: arrays and null slip through
function isFunction(value) { return typeof value === 'function'; }
function isNull(value) { return typeof value === 'object' && value === null; }
function isUndefined(value) { return typeof value === 'undefined'; }
function isNaN(value) { return isNaN(value); } // BUG: infinite recursion + global coerces
Six of the nine are fine. Three break in interesting ways. isNumber(NaN) returns true because typeof NaN really is 'number'. isArray({}) returns true because plain objects and arrays share the 'object' bucket. isObject(null) returns true for the same reason. And isNaN(value) calls itself recursively and would blow the stack — naming your function isNaN shadows the global isNaN, so the isNaN(value) call inside the body resolves to the function itself, not the global, recursing forever. Even if you fixed that to call the global isNaN explicitly, the global version coerces its argument first, so isNaN('foo') returns true. We need three specific fixes.
function isString(value) {
// 'string' is one of the seven typeof buckets — no exceptions, no quirks.
return typeof value === 'string';
}
function isNumber(value) {
// typeof NaN is 'number', but NaN means "not a number" by definition.
// Exclude it explicitly so callers don't get a useless 'true'.
return typeof value === 'number' && !Number.isNaN(value);
}
function isBoolean(value) {
return typeof value === 'boolean';
}
function isArray(value) {
// typeof [] is 'object', so typeof can't help. Array.isArray exists
// exactly to answer this question reliably — including for cross-realm
// arrays (arrays created in a different execution context like an iframe
// or a Web Worker, each of which has its own Array constructor), where
// `value instanceof Array` would return false for a real array.
return Array.isArray(value);
}
function isObject(value) {
// Three gates, each closing a specific lie:
// (1) typeof value === 'object' — value lives in the object bucket
// (2) value !== null — fixes the famous typeof null === 'object' bug
// (3) !Array.isArray(value) — arrays also live in the object bucket; route them to isArray
// All three must hold for a value to count as a "plain object" here.
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
function isFunction(value) {
// 'function' is its own typeof bucket — separate from 'object', no quirks.
return typeof value === 'function';
}
function isNull(value) {
// Strict equality only. == null would also match undefined, which is wrong here.
return value === null;
}
function isUndefined(value) {
// typeof undefined is 'undefined' — but value === undefined also works.
// Both are fine; we pick typeof for symmetry with the rest of the file.
return typeof value === 'undefined';
}
function isNaN(value) {
// Number.isNaN does NOT coerce — only the literal NaN value returns true.
// The global isNaN(x) would return true for 'foo', undefined, {} — anything
// that coerces to NaN. That's almost never what you want.
return Number.isNaN(value);
}
module.exports = {
isString, isNumber, isBoolean, isArray, isObject,
isFunction, isNull, isUndefined, isNaN,
};
The five honest helpers are one-liners. The four that need work are: isNumber ANDs in a !Number.isNaN check; isArray skips typeof entirely and uses Array.isArray; isObject triple-gates with typeof === 'object', value !== null, and !Array.isArray(value); isNaN delegates to Number.isNaN so no coercion happens.
isObject is where most people get burned. The three gates each fix a specific lie:
And isNaN is short but the reasoning matters: the global isNaN and Number.isNaN are not the same function.
Take a handful of calls against the working code:
isNumber(42) — typeof 42 is 'number', so the left side of && is true. Number.isNaN(42) is false, so !Number.isNaN(42) is true. Both sides true → returns true.isNumber(NaN) — typeof NaN is 'number', so the left side is true. Number.isNaN(NaN) is true, so !Number.isNaN(NaN) is false. AND short-circuits → returns false. The NaN trap is closed.isObject({ a: 1 }) — typeof { a: 1 } is 'object' (true). { a: 1 } !== null (true). !Array.isArray({ a: 1 }) (true). All three gates pass → returns true.isObject(null) — typeof null is 'object' (true). null !== null is false. Second gate fails, short-circuits → returns false. The null-is-an-object trap is closed.isObject([1, 2]) — typeof [1, 2] is 'object' (true). [1, 2] !== null (true). !Array.isArray([1, 2]) is !true = false. Third gate fails → returns false. The arrays-are-objects trap is closed.isNaN('foo') — calls Number.isNaN('foo'). No coercion. 'foo' is not the literal NaN value. Returns false. (The global isNaN('foo') would return true here — that's the trap we're avoiding.)isArray([]) — Array.isArray([]) is true. Returns true. Note we skipped typeof entirely — Array.isArray handles the edge cases (cross-realm arrays from iframes, etc.) that a hand-rolled check would miss.typeof null === 'object' — this is the most famous JavaScript bug, baked in since 1995 and unfixable for backwards-compatibility reasons. Any isObject that doesn't explicitly check value !== null will return true for null. Add the check, and add a unit test that names it: expect(isObject(null)).toBe(false).isNaN instead of Number.isNaN — global isNaN coerces, so isNaN('foo') is true, isNaN(undefined) is true, isNaN({}) is true. None of those are NaN; they just become NaN when forced through Number(...). Number.isNaN skips the coercion step and is strict-equal-to-NaN semantics, which is what you almost always want.isNaN — your isNaN shadows the global isNaN inside this module, which is fine and even desirable. But if you ever write return isNaN(value) inside your own isNaN body without Number., you'll get infinite recursion and a stack overflow. Always delegate to Number.isNaN, never to plain isNaN.typeof decide isArray — typeof [] === 'object', so any check that starts with typeof cannot tell arrays from objects. Array.isArray is the only built-in that handles this reliably, including for arrays created in other realms (iframes, workers) where [] instanceof Array would lie.null with undefined — null is an explicit "this is empty" value; undefined is "the slot was never filled." isNull(undefined) must be false, and isUndefined(null) must be false. Use strict equality (===) or typeof, never == null (which matches both).Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.
JavaScript's typeof operator is the standard way to ask what kind of value you're holding — but it has well-known holes. typeof null is "object". typeof [] is "object". typeof NaN is "number". You'll build a small library of type-check helpers that paper over those holes so callers get the answer they actually expected.
Implement and export nine helpers from a single module. Each one takes a value and returns a boolean. The full set: isString, isNumber, isBoolean, isArray, isObject, isFunction, isNull, isUndefined, and isNaN. isObject returns true only for plain objects — not arrays, not null. isNumber returns true only for real numbers — NaN does not count as a number here. Use Number.isNaN for the NaN check; the global isNaN coerces its argument first and is not what you want.
function isString(value) { /* returns boolean */ }
function isNumber(value) { /* returns boolean — NaN is NOT a number */ }
function isBoolean(value) { /* returns boolean */ }
function isArray(value) { /* returns boolean */ }
function isObject(value) { /* returns boolean — null and arrays are NOT objects */ }
function isFunction(value) { /* returns boolean */ }
function isNull(value) { /* returns boolean — only for the value null */ }
function isUndefined(value) { /* returns boolean — only for the value undefined */ }
function isNaN(value) { /* returns boolean — only for the NaN value, no coercion */ }
module.exports = {
isString, isNumber, isBoolean, isArray, isObject,
isFunction, isNull, isUndefined, isNaN,
};
isString('hello'); // true
isString(''); // true — empty string is still a string
isString(new String('hi')); // false — boxed wrapper, not a primitive string
isNumber(42); // true
isNumber(NaN); // false — NaN is excluded from "number" here
isNumber('3'); // false — strings that look like numbers don't count
isObject({}); // true
isObject({ a: 1 }); // true
isObject(null); // false — typeof says 'object' but null is its own thing
isObject([1, 2]); // false — arrays get their own helper
isArray([]); // true
isArray([1, 2, 3]); // true
isArray('abc'); // false — strings are iterable, not arrays
isNull(null); // true
isNull(undefined); // false
isUndefined(undefined); // true
isUndefined(null); // false
isNaN(NaN); // true
isNaN('foo'); // false — no coercion (global isNaN would say true)
isNaN(42); // false
typeof for primitives — 'string', 'number', 'boolean', 'function', 'undefined' are all reliably reported by typeof.Array.isArray for arrays — typeof [] is 'object', so typeof alone can't distinguish arrays from plain objects.isObject against null and arrays — typeof null === 'object' is a historical JavaScript bug; you must explicitly reject null and arrays.Number.isNaN, not the global isNaN — global isNaN('foo') returns true because it coerces first. Number.isNaN only returns true for the literal NaN value.Date, Map, Set, class instances all pass isObject here. Distinguishing them is out of scope for v1.You'll write nine small helpers that mostly delegate to typeof, with three carefully-placed extra checks that paper over the language's three historical quirks: typeof null === 'object', typeof [] === 'object', and typeof NaN === 'number'.
JavaScript has one built-in type-asking operator — typeof — and it's mostly fine. typeof 'hi' is 'string'. typeof 42 is 'number'. typeof true is 'boolean'. But it has three lies baked into the language for backwards-compatibility reasons that nobody can fix at this point. Your job is to write a small set of helpers — isString, isNumber, isBoolean, isArray, isObject, isFunction, isNull, isUndefined, isNaN — that callers can trust without remembering the quirks.
typeof returns one of seven strings: 'string', 'number', 'boolean', 'undefined', 'object', 'function', 'symbol', 'bigint'. Most of those map 1:1 to the helper you want. The trouble is the 'object' and 'number' buckets: each one contains values that shouldn't really live there.
For the honest rows you can write a one-liner. For the three lying rows you need a second check to filter the lie back out.
A reasonable first try is to map each helper directly to typeof:
function isString(value) { return typeof value === 'string'; }
function isNumber(value) { return typeof value === 'number'; } // BUG: NaN slips through
function isBoolean(value) { return typeof value === 'boolean'; }
function isArray(value) { return typeof value === 'object'; } // BUG: {} and null slip through
function isObject(value) { return typeof value === 'object'; } // BUG: arrays and null slip through
function isFunction(value) { return typeof value === 'function'; }
function isNull(value) { return typeof value === 'object' && value === null; }
function isUndefined(value) { return typeof value === 'undefined'; }
function isNaN(value) { return isNaN(value); } // BUG: infinite recursion + global coerces
Six of the nine are fine. Three break in interesting ways. isNumber(NaN) returns true because typeof NaN really is 'number'. isArray({}) returns true because plain objects and arrays share the 'object' bucket. isObject(null) returns true for the same reason. And isNaN(value) calls itself recursively and would blow the stack — naming your function isNaN shadows the global isNaN, so the isNaN(value) call inside the body resolves to the function itself, not the global, recursing forever. Even if you fixed that to call the global isNaN explicitly, the global version coerces its argument first, so isNaN('foo') returns true. We need three specific fixes.
function isString(value) {
// 'string' is one of the seven typeof buckets — no exceptions, no quirks.
return typeof value === 'string';
}
function isNumber(value) {
// typeof NaN is 'number', but NaN means "not a number" by definition.
// Exclude it explicitly so callers don't get a useless 'true'.
return typeof value === 'number' && !Number.isNaN(value);
}
function isBoolean(value) {
return typeof value === 'boolean';
}
function isArray(value) {
// typeof [] is 'object', so typeof can't help. Array.isArray exists
// exactly to answer this question reliably — including for cross-realm
// arrays (arrays created in a different execution context like an iframe
// or a Web Worker, each of which has its own Array constructor), where
// `value instanceof Array` would return false for a real array.
return Array.isArray(value);
}
function isObject(value) {
// Three gates, each closing a specific lie:
// (1) typeof value === 'object' — value lives in the object bucket
// (2) value !== null — fixes the famous typeof null === 'object' bug
// (3) !Array.isArray(value) — arrays also live in the object bucket; route them to isArray
// All three must hold for a value to count as a "plain object" here.
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
function isFunction(value) {
// 'function' is its own typeof bucket — separate from 'object', no quirks.
return typeof value === 'function';
}
function isNull(value) {
// Strict equality only. == null would also match undefined, which is wrong here.
return value === null;
}
function isUndefined(value) {
// typeof undefined is 'undefined' — but value === undefined also works.
// Both are fine; we pick typeof for symmetry with the rest of the file.
return typeof value === 'undefined';
}
function isNaN(value) {
// Number.isNaN does NOT coerce — only the literal NaN value returns true.
// The global isNaN(x) would return true for 'foo', undefined, {} — anything
// that coerces to NaN. That's almost never what you want.
return Number.isNaN(value);
}
module.exports = {
isString, isNumber, isBoolean, isArray, isObject,
isFunction, isNull, isUndefined, isNaN,
};
The five honest helpers are one-liners. The four that need work are: isNumber ANDs in a !Number.isNaN check; isArray skips typeof entirely and uses Array.isArray; isObject triple-gates with typeof === 'object', value !== null, and !Array.isArray(value); isNaN delegates to Number.isNaN so no coercion happens.
isObject is where most people get burned. The three gates each fix a specific lie:
And isNaN is short but the reasoning matters: the global isNaN and Number.isNaN are not the same function.
Take a handful of calls against the working code:
isNumber(42) — typeof 42 is 'number', so the left side of && is true. Number.isNaN(42) is false, so !Number.isNaN(42) is true. Both sides true → returns true.isNumber(NaN) — typeof NaN is 'number', so the left side is true. Number.isNaN(NaN) is true, so !Number.isNaN(NaN) is false. AND short-circuits → returns false. The NaN trap is closed.isObject({ a: 1 }) — typeof { a: 1 } is 'object' (true). { a: 1 } !== null (true). !Array.isArray({ a: 1 }) (true). All three gates pass → returns true.isObject(null) — typeof null is 'object' (true). null !== null is false. Second gate fails, short-circuits → returns false. The null-is-an-object trap is closed.isObject([1, 2]) — typeof [1, 2] is 'object' (true). [1, 2] !== null (true). !Array.isArray([1, 2]) is !true = false. Third gate fails → returns false. The arrays-are-objects trap is closed.isNaN('foo') — calls Number.isNaN('foo'). No coercion. 'foo' is not the literal NaN value. Returns false. (The global isNaN('foo') would return true here — that's the trap we're avoiding.)isArray([]) — Array.isArray([]) is true. Returns true. Note we skipped typeof entirely — Array.isArray handles the edge cases (cross-realm arrays from iframes, etc.) that a hand-rolled check would miss.typeof null === 'object' — this is the most famous JavaScript bug, baked in since 1995 and unfixable for backwards-compatibility reasons. Any isObject that doesn't explicitly check value !== null will return true for null. Add the check, and add a unit test that names it: expect(isObject(null)).toBe(false).isNaN instead of Number.isNaN — global isNaN coerces, so isNaN('foo') is true, isNaN(undefined) is true, isNaN({}) is true. None of those are NaN; they just become NaN when forced through Number(...). Number.isNaN skips the coercion step and is strict-equal-to-NaN semantics, which is what you almost always want.isNaN — your isNaN shadows the global isNaN inside this module, which is fine and even desirable. But if you ever write return isNaN(value) inside your own isNaN body without Number., you'll get infinite recursion and a stack overflow. Always delegate to Number.isNaN, never to plain isNaN.typeof decide isArray — typeof [] === 'object', so any check that starts with typeof cannot tell arrays from objects. Array.isArray is the only built-in that handles this reliably, including for arrays created in other realms (iframes, workers) where [] instanceof Array would lie.null with undefined — null is an explicit "this is empty" value; undefined is "the slot was never filled." isNull(undefined) must be false, and isUndefined(null) must be false. Use strict equality (===) or typeof, never == null (which matches both).Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.