JavaScript: Validation
Checking input the way a real form has to.
10 exercises
1 Is the value a boolean?
easyWrite a check that is true only for true and false. Not for truthy values, and not for a boolean wrapper object.
Stuck? Three hints, smallest first
- 1.
typeof v === ‘boolean’gets the ordinary cases right. - 2.
new Boolean(false)is an object, and every object is truthy — including that one. - 3.
Object.prototype.toString.call(v)sees through the wrapper.
// The tag trick, used all through this section. It returns a string like
// 'Boolean', 'Null', 'Array' — and unlike typeof it is never wrong.
const tag = (v) => Object.prototype.toString.call(v).slice(8, -1);
// Good enough almost always: rejects wrapper objects too, since typeof
// new Boolean(true) is 'object'.
const isBoolean = (v) => typeof v === 'boolean';
// Accepts wrappers as well, if you have to deal with them.
const isBooleanLike = (v) => tag(v) === 'Boolean';
const wrapped = new Boolean(false);
console.log('typeof true :', typeof true);
console.log('typeof new Boolean() :', typeof wrapped, '<- an object, not a boolean');
console.log('tag(new Boolean()) :', tag(wrapped));
console.log('is new Boolean(false) truthy?', wrapped ? 'yes' : 'no', '<- objects always are');
console.log('so !!wrapped is', !!wrapped, 'while wrapped.valueOf() is', wrapped.valueOf());
Output
11 checks that prove this answer
You do not have to read these. They are part of the answer panel above, so pressing
Run there executes all 11 of them and prints ok for each.
That output is the proof, and the build fails if a single check fails.
test('true is a boolean', isBoolean(true), true);
test('false is a boolean', isBoolean(false), true);
test('0 is not', isBoolean(0), false);
test('the string "true" is not', isBoolean('true'), false);
test('null is not', isBoolean(null), false);
test('undefined is not', isBoolean(undefined), false);
test('a wrapper object is not a real boolean', isBoolean(wrapped), false);
test('but the tag check recognises it', isBooleanLike(wrapped), true);
test('new Boolean(false) is truthy, which is the whole danger', wrapped ? true : false, true);
test('while its actual value is false', wrapped.valueOf(), false);
test('truthiness is a different question from type', isBoolean(1), false);
typeof v === 'boolean' is the right answer here, and the reason to write the exercise anyway is new Boolean(false). It is an object, every object is truthy, so if (new Boolean(false)) runs the true branch. You will not write that on purpose, but code that does new Boolean(someValue) to "convert" it produces exactly this. Use Boolean(v) or !!v to convert — never new.
Answers JS Validation 1 in the original set.
2 Is the value an error?
fairDetect an Error, including the subclasses like TypeError and your own custom ones. Then find where instanceof gives the wrong answer.
Stuck? Three hints, smallest first
- 1.
instanceof Errorcovers every subclass, because they all inherit from it. - 2. It compares against one particular
Errorconstructor. A value from an iframe or worker has a different one. - 3. The tag check is
‘Error’for every built-in error type, includingTypeError.
const tag = (v) => Object.prototype.toString.call(v).slice(8, -1);
const isError = (v) => v instanceof Error;
// Survives crossing between windows, workers or iframes, where the value was
// built by a *different* Error constructor and instanceof therefore fails.
const isErrorLike = (v) => tag(v) === 'Error';
class TicketError extends Error {
constructor(message) {
super(message);
this.name = 'TicketError';
}
}
// A hand-made lookalike: right shape, wrong ancestry. This is the case that
// separates the two checks.
const fake = { name: 'Error', message: 'not really an error', stack: 'made up' };
// Simulating the cross-realm problem without a second window: an object whose
// prototype chain does not include this realm's Error.
const foreign = Object.create(null);
foreign.name = 'Error';
foreign.message = 'from another realm';
console.log('tag(new Error) :', tag(new Error('x')));
console.log('tag(new TypeError) :', tag(new TypeError('x')), '<- also "Error"');
console.log('tag(new TicketError):', tag(new TicketError('x')));
console.log('name of a subclass :', new TypeError('x').name);
console.log('the lookalike passes instanceof?', isError(fake));
console.log('the lookalike passes the tag check?', isErrorLike(fake));
Output
13 checks that prove this answer
You do not have to read these. They are part of the answer panel above, so pressing
Run there executes all 13 of them and prints ok for each.
That output is the proof, and the build fails if a single check fails.
test('a plain Error', isError(new Error('boom')), true);
test('a TypeError, because it inherits', isError(new TypeError('boom')), true);
test('a RangeError too', isError(new RangeError('boom')), true);
test('a custom subclass', isError(new TicketError('no seats')), true);
test('a string is not an error', isError('boom'), false);
test('null is not', isError(null), false);
test('an object shaped like one is not', isError(fake), false);
test('the tag is "Error" for every built-in type', ['Error', 'TypeError', 'RangeError'].map((n) => tag(new globalThis[n]('x'))), ['Error', 'Error', 'Error']);
test('so the tag cannot tell you which kind it is', tag(new TypeError('x')) === tag(new RangeError('x')), true);
test('the name property can', new TypeError('x').name, 'TypeError');
test('and a subclass sets its own', new TicketError('x').name, 'TicketError');
test('the tag check also rejects the lookalike', isErrorLike(fake), false);
test('a subclass is still an instance of its own class', new TicketError('x') instanceof TicketError, true);
instanceof Error is what to reach for, and it has one real failure: it checks against the Error constructor of the current window. An error thrown inside an iframe or a web worker was built by a different constructor, so instanceof says no even though it plainly is an error. The tag check has the opposite tradeoff — it works across realms, but it flattens TypeError and RangeError both to 'Error', so use .name when you need the specific kind.
Answers JS Validation 2 in the original set.
3 Is the value NaN?
fairDetect the value NaN. This is harder than it sounds: NaN is not equal to itself, and the old global isNaN answers a different question than the one you asked.
Stuck? Three hints, smallest first
- 1.
NaN === NaNisfalse. It is the only value in the language not equal to itself. - 2. The global
isNaN(v)convertsvto a number first, soisNaN(‘abc’)istrue. - 3.
Number.isNaNdoes no conversion, andv !== vis the same test without any function call.
// The correct check. No conversion: it is true only for actual NaN.
const isReallyNaN = (v) => Number.isNaN(v);
// The same test written by hand, and the reason it works: NaN is the only value
// in JavaScript that is not equal to itself.
const notItself = (v) => v !== v;
console.log('NaN === NaN :', NaN === NaN, '<- the whole problem');
console.log('typeof NaN :', typeof NaN, '<- it IS a number');
console.log('--- the global isNaN converts first, so it lies ---');
for (const v of ['abc', undefined, {}, '', [], '12abc']) {
console.log(String(JSON.stringify(v)).padEnd(8), 'isNaN:', String(isNaN(v)).padEnd(6), 'Number.isNaN:', Number.isNaN(v));
}
console.log('--- where NaN actually comes from ---');
console.log("Number('abc') =", Number('abc'));
console.log('0 / 0 =', 0 / 0);
console.log('Math.sqrt(-1) =', Math.sqrt(-1));
console.log("parseInt('abc')=", parseInt('abc'));
console.log("Number('') =", Number(''), '<- NOT NaN. Empty string is zero.');
Output
15 checks that prove this answer
You do not have to read these. They are part of the answer panel above, so pressing
Run there executes all 15 of them and prints ok for each.
That output is the proof, and the build fails if a single check fails.
test('NaN is NaN', isReallyNaN(NaN), true);
test('and it is famously not equal to itself', NaN === NaN, false);
test('its type is number, which surprises people', typeof NaN, 'number');
test('the string "abc" is not NaN, it is a string', isReallyNaN('abc'), false);
test('but the global isNaN says true, because it converts first', isNaN('abc'), true);
test('the global also fails on undefined', isNaN(undefined), true);
test('while Number.isNaN is correct', Number.isNaN(undefined), false);
test('the empty string converts to 0, not NaN', Number(''), 0);
test('so isNaN("") is false', isNaN(''), false);
test('an empty array also converts to 0', isNaN([]), false);
test('v !== v is the same test', notItself(NaN), true);
test('and agrees on every case', [NaN, 1, 'abc', undefined, null, {}, ''].every((v) => notItself(v) === Number.isNaN(v)), true);
test('Number("abc") really is NaN', isReallyNaN(Number('abc')), true);
test('0 / 0 is NaN, not an error', isReallyNaN(0 / 0), true);
test('Object.is spots it as well', Object.is(NaN, NaN), true);
Three separate facts collide here. NaN has type 'number', so a typeof guard lets it through. It is not equal to itself, so v === NaN never matches anything. And the global isNaN means "is this not a number after conversion", which is a different question — isNaN('abc') is true even though 'abc' is a perfectly good string. Use Number.isNaN. And note Number('') is 0, so an empty form field sails through a naive numeric check as a valid zero.
Answers JS Validation 3 in the original set.
4 Is the value null?
easyDetect null specifically, and not undefined. The obvious typeof approach cannot do it.
Stuck? Three hints, smallest first
- 1.
typeof nullis‘object’. This is a bug from 1995 that can never be fixed. - 2.
v === nullis the whole check. Use===, becausenull == undefinedis true. - 3. To catch both at once,
v == nullis the idiomatic shorthand.
const tag = (v) => Object.prototype.toString.call(v).slice(8, -1);
const isNull = (v) => v === null; // strict: only null
const isNullish = (v) => v == null; // loose: null OR undefined
const isUndefined = (v) => v === undefined;
console.log('typeof null :', typeof null, '<- the famous mistake');
console.log('tag(null) :', tag(null), '<- the tag gets it right');
console.log('null == undefined:', null == undefined, '<- loose equality merges them');
console.log('null === undefined:', null === undefined);
console.log('--- and they behave differently ---');
const bus = { plate: 'LAG-441-XA', driver: null };
console.log('a null property :', bus.driver, '(deliberately empty)');
console.log('a missing property:', bus.conductor, '(never set)');
console.log("'driver' in bus :", 'driver' in bus);
console.log("'conductor' in bus:", 'conductor' in bus);
console.log('JSON keeps null, drops undefined:', JSON.stringify({ a: null, b: undefined }));
Output
15 checks that prove this answer
You do not have to read these. They are part of the answer panel above, so pressing
Run there executes all 15 of them and prints ok for each.
That output is the proof, and the build fails if a single check fails.
test('null is null', isNull(null), true);
test('undefined is not null', isNull(undefined), false);
test('0 is not null', isNull(0), false);
test('the empty string is not null', isNull(''), false);
test('typeof null is "object", so typeof cannot be used', typeof null, 'object');
test('the tag check knows better', tag(null), 'Null');
test('loose equality merges null and undefined', null == undefined, true);
test('strict equality does not', null === undefined, false);
test('the nullish check catches both', [null, undefined].every(isNullish), true);
test('and nothing else', [0, '', false, NaN].some(isNullish), false);
test('a deliberately empty property is null', isNull(bus.driver), true);
test('a property that was never set is undefined', isUndefined(bus.conductor), true);
test('but the key exists for the null one', 'driver' in bus, true);
test('and not for the other', 'conductor' in bus, false);
test('JSON.stringify keeps null and drops undefined', JSON.stringify({ a: null, b: undefined }), '{"a":null}');
typeof null === 'object' is a genuine bug in the first JavaScript implementation. A proposal to fix it was rejected because too much existing code depends on it, so it is permanent. Practically, keep the distinction: null means "empty on purpose", undefined means "never set". JSON.stringify takes that seriously — it keeps null and silently removes undefined, so a round-trip through JSON loses one and not the other.
Answers JS Validation 4 in the original set.
5 Is the value a number?
fairCheck whether a value is a number. Decide first what you actually mean, because NaN and Infinity both pass typeof v === 'number'.
Stuck? Three hints, smallest first
- 1.
typeof v === ‘number’is true forNaNand forInfinity. - 2.
Number.isFiniteis usually the check you want: a real, usable number. - 3. The global
isFiniteconverts first, soisFinite(‘5’)is true.Number.isFinitedoes not.
// Literally "is of type number" — includes NaN and Infinity.
const isNumberType = (v) => typeof v === 'number';
// What people usually mean: a number you can do arithmetic with.
const isUsableNumber = (v) => typeof v === 'number' && Number.isFinite(v);
// For text input: is this string a number? Note the trim guard, because
// Number('') and Number(' ') are both 0.
const isNumericString = (s) =>
typeof s === 'string' && s.trim() !== '' && Number.isFinite(Number(s));
const wrapped = new Number(5);
console.log('typeof NaN :', typeof NaN, ' isUsable:', isUsableNumber(NaN));
console.log('typeof Infinity :', typeof Infinity, ' isUsable:', isUsableNumber(Infinity));
console.log("typeof '5' :", typeof '5', ' isUsable:', isUsableNumber('5'));
console.log('new Number(5) :', typeof wrapped, '<- an object');
console.log('--- global isFinite converts, Number.isFinite does not ---');
for (const v of ['5', '', ' ', [], null, true]) {
console.log(JSON.stringify(v).padEnd(6), 'isFinite:', String(isFinite(v)).padEnd(6), 'Number.isFinite:', Number.isFinite(v));
}
Output
18 checks that prove this answer
You do not have to read these. They are part of the answer panel above, so pressing
Run there executes all 18 of them and prints ok for each.
That output is the proof, and the build fails if a single check fails.
test('42 is a number', isUsableNumber(42), true);
test('so is 0', isUsableNumber(0), true);
test('and a decimal', isUsableNumber(-3.5), true);
test('NaN has type number', isNumberType(NaN), true);
test('but it is not usable', isUsableNumber(NaN), false);
test('Infinity has type number too', isNumberType(Infinity), true);
test('and is also not usable', isUsableNumber(Infinity), false);
test('the string "5" is not a number', isUsableNumber('5'), false);
test('a wrapper object is not either', isUsableNumber(wrapped), false);
test('the global isFinite is fooled by a numeric string', isFinite('5'), true);
test('Number.isFinite is not', Number.isFinite('5'), false);
test('the global is even fooled by the empty string', isFinite(''), true);
test('because Number("") is 0', Number(''), 0);
test('the string check accepts a numeric string', isNumericString('5'), true);
test('and a decimal one', isNumericString('-3.5'), true);
test('but refuses an empty field', isNumericString(''), false);
test('and a field of only spaces', isNumericString(' '), false);
test('and text', isNumericString('abc'), false);
"Is it a number" is three different questions. typeof answers the type question and lets NaN and Infinity through — both of which will poison any arithmetic downstream. Number.isFinite answers the useful one. And validating text from a form is a third thing entirely, where the trim() !== '' guard is essential: Number(' ') is 0, so a field containing only spaces otherwise validates as the number zero.
Answers JS Validation 5 in the original set.
6 Is the value an object?
fairCheck whether a value is an object. Two traps: typeof null is 'object', and a function is an object even though typeof says otherwise.
Stuck? Three hints, smallest first
- 1. Rule out
nullexplicitly, becausetypeof null === ‘object’. - 2. Arrays, dates and regular expressions are all objects. Decide whether you want them.
- 3.
typeof fnis‘function’, but a function really is an object — you can put properties on it.
const tag = (v) => Object.prototype.toString.call(v).slice(8, -1);
// Anything you can hold properties on, functions included.
const isObject = (v) => (typeof v === 'object' && v !== null) || typeof v === 'function';
// Objects only, no functions.
const isNonFunctionObject = (v) => typeof v === 'object' && v !== null;
// Proof that a function is an object: it can carry properties.
function fare() {}
fare.currency = 'NGN';
console.log('typeof {} :', typeof {});
console.log('typeof [] :', typeof [], '<- also "object"');
console.log('typeof null :', typeof null, '<- also "object", and it is not one');
console.log('typeof fare :', typeof fare, '<- "function", but it IS an object');
console.log('a property on a function:', fare.currency);
console.log('--- the tag tells them apart ---');
for (const v of [{}, [], null, new Date(), /x/, fare, new Map()]) {
console.log(tag(v).padEnd(10), 'isObject:', isObject(v));
}
Output
17 checks that prove this answer
You do not have to read these. They are part of the answer panel above, so pressing
Run there executes all 17 of them and prints ok for each.
That output is the proof, and the build fails if a single check fails.
test('a plain object', isObject({}), true);
test('an array', isObject([]), true);
test('a date', isObject(new Date()), true);
test('a regular expression', isObject(/x/), true);
test('a Map', isObject(new Map()), true);
test('a function counts as an object', isObject(fare), true);
test('because it can carry properties', fare.currency, 'NGN');
test('null does NOT count', isObject(null), false);
test('even though typeof says object', typeof null, 'object');
test('a number is not an object', isObject(42), false);
test('nor a string', isObject('bus'), false);
test('nor undefined', isObject(undefined), false);
test('the stricter check excludes functions', isNonFunctionObject(fare), false);
test('but still accepts an array', isNonFunctionObject([]), true);
test('Array.isArray separates arrays out', Array.isArray([]), true);
test('and is not fooled by an array-like object', Array.isArray({ length: 0 }), false);
test('the tag distinguishes what typeof cannot', [{}, [], new Date(), /x/].map(tag), ['Object', 'Array', 'Date', 'RegExp']);
typeof v === 'object' && v !== null is the standard guard, and the null half is not optional — without it, isObject(null) returns true and the next line that reads a property throws. Whether functions should count depends on the question: they are objects (the fare.currency assertion proves it), but if you are about to enumerate keys or serialise the value, you probably want them excluded. Array.isArray is the one built-in worth remembering; it is reliable and it works across realms.
Answers JS Validation 6 in the original set.
7 Is it a plain object?
hardDistinguish a plain {} — the kind that comes out of JSON.parse — from a class instance, an array, a date, or anything else that happens to be an object.
Stuck? Three hints, smallest first
- 1. Check the prototype: a plain object’s is exactly
Object.prototype. - 2.
Object.create(null)has no prototype and is arguably the plainest object of all. - 3. A class instance has its own prototype in between, so the direct comparison fails for it.
const tag = (v) => Object.prototype.toString.call(v).slice(8, -1);
function isPlainObject(v) {
if (tag(v) !== 'Object') return false; // rules out arrays, dates, null, Map...
const proto = Object.getPrototypeOf(v);
// Either a normal {} literal, or a prototype-less bag from Object.create(null).
return proto === Object.prototype || proto === null;
}
// Everything JSON can produce is plain, so this is a good practical test.
const isJsonObject = (v) => isPlainObject(v) && (() => {
try { return JSON.stringify(v) !== undefined; } catch { return false; }
})();
class Bus {
constructor(plate) { this.plate = plate; }
}
const literal = { plate: 'LAG-441-XA' };
const bare = Object.create(null);
bare.plate = 'ABJ-772-KM';
const instance = new Bus('ENU-318-PH');
const parsed = JSON.parse('{"plate":"KAN-905-BD"}');
console.log('prototype of {} ===', Object.getPrototypeOf(literal) === Object.prototype);
console.log('prototype of Object.create(null) ===', Object.getPrototypeOf(bare));
console.log('prototype of new Bus() ===', Object.getPrototypeOf(instance) === Object.prototype, '<- Bus.prototype sits in between');
console.log('tag of a class instance :', tag(instance), '<- still "Object", so the tag alone is not enough');
console.log('--- and the bare object has no inherited methods at all ---');
console.log('literal.toString exists?', typeof literal.toString);
console.log('bare.toString exists? ', typeof bare.toString);
Output
16 checks that prove this answer
You do not have to read these. They are part of the answer panel above, so pressing
Run there executes all 16 of them and prints ok for each.
That output is the proof, and the build fails if a single check fails.
test('an object literal is plain', isPlainObject(literal), true);
test('a JSON.parse result is plain', isPlainObject(parsed), true);
test('a prototype-less object is plain', isPlainObject(bare), true);
test('a class instance is NOT', isPlainObject(instance), false);
test('even though its tag says Object', tag(instance), 'Object');
test('an array is not plain', isPlainObject([]), false);
test('nor a date', isPlainObject(new Date()), false);
test('nor a Map', isPlainObject(new Map()), false);
test('nor null', isPlainObject(null), false);
test('nor a function', isPlainObject(() => {}), false);
test('a nested literal is still plain', isPlainObject({ a: { b: 1 } }), true);
test('a class instance has its own prototype in the chain', Object.getPrototypeOf(instance) === Bus.prototype, true);
test('a bare object inherits nothing, not even toString', typeof bare.toString, 'undefined');
test('while a literal does', typeof literal.toString, 'function');
test('the bare object still holds its data', bare.plate, 'ABJ-772-KM');
test('and the JSON check agrees with the plain check here', isJsonObject(literal), true);
The prototype is the only thing that separates { plate: 'X' } from new Bus('X') — both have tag 'Object' and both have a plate property. This matters when you are merging configuration or deep-cloning: recursing into a class instance as though it were a plain bag of data usually destroys it.
Object.create(null) is worth knowing separately. It inherits nothing, so it has no toString and no hasOwnProperty — the assertions above check that. It is the safe choice for a lookup table built from user-supplied keys, because a key called "toString" or "__proto__" cannot then collide with anything inherited.
Answers JS Validation 7 in the original set.
8 Is the value a regular expression?
easyDetect a RegExp, whether written as /x/ or built with new RegExp('x'). Then note the stateful trap that makes regexes worth checking for.
Stuck? Three hints, smallest first
- 1. The literal form and the constructor form produce the same kind of object.
- 2.
instanceof RegExpworks; the tag check also works and crosses realms. - 3. A regex with the
gflag remembers its position inlastIndexbetween calls.
const tag = (v) => Object.prototype.toString.call(v).slice(8, -1);
const isRegExp = (v) => tag(v) === 'RegExp';
const literal = /LAG-\d{3}-[A-Z]{2}/;
const built = new RegExp('LAG-\\d{3}-[A-Z]{2}');
console.log('literal :', literal, ' tag:', tag(literal));
console.log('constructed:', built, ' tag:', tag(built));
console.log('same source?', literal.source === built.source);
console.log('--- the trap: a /g regex keeps state between calls ---');
const global = /a/g;
console.log('first test("banana"):', global.test('banana'), 'lastIndex now', global.lastIndex);
console.log('second test("banana"):', global.test('banana'), 'lastIndex now', global.lastIndex);
console.log('third test("banana"):', global.test('banana'), 'lastIndex now', global.lastIndex);
console.log('fourth test("banana"):', global.test('banana'), '<- false, and it reset');
console.log('a non-global regex is stateless:', /a/.test('banana'), /a/.test('banana'));
Output
16 checks that prove this answer
You do not have to read these. They are part of the answer panel above, so pressing
Run there executes all 16 of them and prints ok for each.
That output is the proof, and the build fails if a single check fails.
test('a regex literal', isRegExp(literal), true);
test('a constructed regex', isRegExp(built), true);
test('an empty one', isRegExp(new RegExp('')), true);
test('a string that looks like one is not', isRegExp('/abc/'), false);
test('an object is not', isRegExp({}), false);
test('null is not', isRegExp(null), false);
test('a function is not', isRegExp(() => {}), false);
test('instanceof agrees for same-realm values', literal instanceof RegExp, true);
test('both forms have the same source', literal.source === built.source, true);
test('and both match the same text', literal.test('LAG-441-XA') === built.test('LAG-441-XA'), true);
test('flags are readable', /a/gi.flags, 'gi');
test('and the global flag is visible', /a/g.global, true);
test('a fresh non-global regex starts at 0', /a/.lastIndex, 0);
test('a global regex advances lastIndex after a match', (() => { const r = /a/g; r.test('banana'); return r.lastIndex; })(), 2);
test('so repeated tests on the same string eventually fail', (() => { const r = /a/g; return [r.test('banana'), r.test('banana'), r.test('banana'), r.test('banana')]; })(), [true, true, true, false]);
test('while a non-global one always agrees with itself', [/a/.test('banana'), /a/.test('banana')], [true, true]);
Detecting a regex is easy; the reason to care is the g flag. A global regex stores a cursor in lastIndex, so calling .test() on the same string four times gives true, true, true, false — the assertion above pins that down. A regex reused across calls, or stored in a module-level constant, will therefore give different answers on identical input. Either drop the g flag when using .test(), or reset lastIndex yourself.
Answers JS Validation 8 in the original set.
9 Is the value a single character?
fairJavaScript has no character type, so "is it a char" means "is it a string of length 1". Then find out why length is the wrong measure.
Stuck? Three hints, smallest first
- 1. Start with
typeof v === ‘string’ && v.length === 1. - 2. An emoji has a
lengthof 2, becauselengthcounts 16-bit units, not characters. - 3. Spreading a string with
[...v]iterates by code point, which is closer to what a person means.
// The literal reading: one 16-bit unit.
const isCharByLength = (v) => typeof v === 'string' && v.length === 1;
// What a person means by "one character": one code point.
const isSingleCodePoint = (v) => typeof v === 'string' && [...v].length === 1;
const emoji = '\u{1F600}'; // a grinning face
const naira = '\u20A6'; // the naira sign
const eAcute = 'e\u0301'; // 'e' plus a combining accent
console.log('"a" length:', 'a'.length, ' code points:', [...'a'].length);
console.log('naira length:', naira.length, ' code points:', [...naira].length, ' ->', naira);
console.log('emoji length:', emoji.length, ' code points:', [...emoji].length, ' ->', emoji);
console.log('e+accent len :', eAcute.length, ' code points:', [...eAcute].length, ' ->', eAcute);
console.log('normalised :', eAcute.normalize('NFC').length, '<- NFC combines them into one');
console.log('emoji charCodeAt(0):', emoji.charCodeAt(0), '<- half a character');
console.log('emoji codePointAt(0):', emoji.codePointAt(0), '<- the whole thing');
Output
18 checks that prove this answer
You do not have to read these. They are part of the answer panel above, so pressing
Run there executes all 18 of them and prints ok for each.
That output is the proof, and the build fails if a single check fails.
test('"a" is one character', isCharByLength('a'), true);
test('"ab" is not', isCharByLength('ab'), false);
test('the empty string is not', isCharByLength(''), false);
test('a number is not a character', isCharByLength(1), false);
test('nor is a one-item array', isCharByLength(['a']), false);
test('the naira sign fits in one unit', isCharByLength(naira), true);
test('an emoji does NOT, by length', isCharByLength(emoji), false);
test('because its length is 2', emoji.length, 2);
test('but it is a single code point', isSingleCodePoint(emoji), true);
test('and the two checks disagree on it', isCharByLength(emoji) === isSingleCodePoint(emoji), false);
test('they agree on plain letters', ['a', 'z', 'A'].every((c) => isCharByLength(c) === isSingleCodePoint(c)), true);
test('e plus a combining accent is two code points', [...eAcute].length, 2);
test('so neither check calls it one character', isSingleCodePoint(eAcute), false);
test('until it is normalised', isSingleCodePoint(eAcute.normalize('NFC')), true);
test('charCodeAt gives half the emoji', emoji.charCodeAt(0), 55357);
test('codePointAt gives all of it', emoji.codePointAt(0), 128512);
test('split("") breaks the emoji apart', emoji.split('').length, 2);
test('spreading keeps it whole', [...emoji].length, 1);
length counts UTF-16 units, not characters. Most letters are one unit, so the difference stays invisible until an emoji or a less common script arrives — then '😀'.length is 2 and split('') tears it into two meaningless halves. Use [...v] or Array.from(v) to iterate by code point.
Even that is not the end of it. e plus a combining accent is two code points that display as one mark, which is why the exercise also shows normalize('NFC'). Any input that will be compared or measured should be normalised first, or two strings that look identical will not be equal.
Answers JS Validation 9 in the original set.
10 Are two values the same?
hardCompare two values. Cover the three levels: ===, Object.is, and a deep comparison that looks inside objects and arrays.
Stuck? Three hints, smallest first
- 1.
===gets two things wrong:NaN === NaNis false, and0 === -0is true. - 2.
Object.isfixes exactly those two cases and nothing else. - 3. Neither looks inside objects, so
{a:1} === {a:1}is false. That needs recursion.
// Level 2: === with its two oddities corrected. This is what Object.is does.
const sameValue = (a, b) => Object.is(a, b);
// Level 3: recurse into arrays and plain objects.
function deepEqual(a, b) {
if (Object.is(a, b)) return true; // handles NaN and -0 correctly
if (typeof a !== 'object' || typeof b !== 'object') return false;
if (a === null || b === null) return false;
if (Array.isArray(a) !== Array.isArray(b)) return false;
const aKeys = Object.keys(a);
const bKeys = Object.keys(b);
if (aKeys.length !== bKeys.length) return false;
return aKeys.every((k) => Object.hasOwn(b, k) && deepEqual(a[k], b[k]));
}
console.log('--- the two cases === gets wrong ---');
console.log('NaN === NaN :', NaN === NaN, ' Object.is:', Object.is(NaN, NaN));
console.log('0 === -0 :', 0 === -0, ' Object.is:', Object.is(0, -0));
console.log('--- neither looks inside ---');
console.log('{a:1} === {a:1} :', { a: 1 } === { a: 1 }, ' deepEqual:', deepEqual({ a: 1 }, { a: 1 }));
console.log('--- and why 1/-0 matters at all ---');
console.log('1 / 0 =', 1 / 0);
console.log('1 / -0 =', 1 / -0, '<- so the sign of zero is not decoration');
Output
22 checks that prove this answer
You do not have to read these. They are part of the answer panel above, so pressing
Run there executes all 22 of them and prints ok for each.
That output is the proof, and the build fails if a single check fails.
test('two equal numbers', sameValue(1, 1), true);
test('two equal strings', sameValue('a', 'a'), true);
test('NaN equals NaN under Object.is', sameValue(NaN, NaN), true);
test('but not under ===', NaN === NaN, false);
test('0 and -0 differ under Object.is', sameValue(0, -0), false);
test('but not under ===', 0 === -0, true);
test('and the difference is real: 1/-0 is -Infinity', 1 / -0, -Infinity);
test('null equals null', sameValue(null, null), true);
test('null does not equal undefined', sameValue(null, undefined), false);
test('two separate objects are not the same value', sameValue({}, {}), false);
test('but they are deeply equal', deepEqual({}, {}), true);
test('matching contents are deeply equal', deepEqual({ a: 1, b: 'x' }, { a: 1, b: 'x' }), true);
test('key order does not matter', deepEqual({ a: 1, b: 2 }, { b: 2, a: 1 }), true);
test('a different value is not equal', deepEqual({ a: 1 }, { a: 2 }), false);
test('an extra key is not equal', deepEqual({ a: 1 }, { a: 1, b: 2 }), false);
test('nesting is handled', deepEqual({ a: { b: [1, 2] } }, { a: { b: [1, 2] } }), true);
test('and a nested difference is caught', deepEqual({ a: { b: [1, 2] } }, { a: { b: [1, 3] } }), false);
test('arrays compare by contents', deepEqual([1, 2, 3], [1, 2, 3]), true);
test('order matters in an array', deepEqual([1, 2], [2, 1]), false);
test('an array is not the same as an object with number keys', deepEqual([1], { 0: 1 }), false);
test('NaN inside an object is handled', deepEqual({ a: NaN }, { a: NaN }), true);
test('the same reference is equal to itself', (() => { const o = { a: 1 }; return sameValue(o, o); })(), true);
Object.is differs from === in exactly two places: NaN now equals itself, and 0 no longer equals -0. That second one looks pedantic until you divide by it — 1 / -0 is -Infinity, so the sign survives and can change a result.
The deep comparison is where the real decisions are. Using Object.is as its base case is what makes { a: NaN } equal itself, which a ===-based version gets wrong. The Array.isArray(a) !== Array.isArray(b) line stops [1] matching { 0: 1 }, which have the same keys and values. And this version compares own enumerable keys only, so it does not handle dates, Maps, Sets or cycles — a real deep-equal has to decide about each of those, which is why the same() helper used by these tests takes the cheap route of comparing JSON strings.
Answers JS Validation 10 in the original set.
The exercise list follows w3resource's JavaScript exercises (CC BY-NC 4.0), and each exercise names the one it answers. The wording, hints, solutions and explanations here are my own.