As a general guideline, we must employ a technique called Duck Typing first (default). If that is not sufficient for our functions, only then must we employ stricter checks.
"If it walks like a duck and it quacks like a duck then it must be a duck"
What this statement means is that we don't always have to check for specific types. Instead, we can check for specific properties, methods and other characteristics that our code depends on (important!)
The duck is that analogy for the type we are concerned with. It can be an object, array, etc - any type. Quacking and walking are traits (characteristics and properties) that we are concerned with.
Why should we use duck typing?
We must use it to improve the FLEXIBILITY of our code. What it means can be explained with the use of an example. Let's say that we have an array and an array-like object (such as an iterable). Let the purpose of our code be to stringify the items the input contains. In this scenario, we don't care if the input is actually an array as long as it is not null (indicating an array like interface) and items that can be iterated.
// Bad!
const logItems = items => {
if (!(items instanceof Array)) {
return
}
for (let item of items) {
console.log(`item: ${item}`)
}
}
const itemsArr = ['Laptop', 'Mouse', 'Charger']
const itemsObj = {
*[Symbol.iterator]() {
yield 'xLaptop'
yield 'xMouse'
yield 'xCharger'
}
}
logItems(itemsArr)
logItems(itemsObj)
// Good!
function isIterable(obj) {
return (
obj != null &&
typeof obj[Symbol.iterator] === 'function'
)
}
const logItems = items => {
if (!isIterable(items)) {
return
}
for (let item of items) {
console.log(`item: ${item}`)
}
}
const itemsArr = ['Laptop', 'Mouse', 'Charger']
const itemsObj = {
*[Symbol.iterator]() {
yield 'xLaptop'
yield 'xMouse'
yield 'xCharger'
}
}
logItems(itemsArr)
logItems(itemsObj)
Another example with objects:
// Bad!
class Person {
constructor(details) {
this.details = details
}
}
function getPersonDetails(person) {
if (!(person instanceof Person)) {
return
}
return person.details
}
const pushkar = new Person({ name: 'Pushkar' })
getPersonDetails(pushkar)
// Good!
class Person {
constructor(details) {
this.details = details
}
}
function getPersonDetails(person) {
if (!(person || person.details)) {
return
}
return person.details
}
const pushkar = new Person({ name: 'Pushkar' })
getPersonDetails(pushkar)
Another example would be duck typing that includes the DOM lists that are array-like objects.
If you really need stricter checks i.e An array must be of type Array and so on, you may use the following techniques:
- Booleans:
typeof value
andvalue === true
orvalue === false
- Numbers:
typeof value
andNumber.isNaN(value)
orNumber.isFinite(value)
forNaN
and bounds (If you want to implicitly typecast the value being checked, use the globalisNaN
andisFinite
) - Strings:
typeof value
andvalue.length
for non-empty string (i.e Disallow''
) - undefined:
typeof value === "undefined"
or the less understoodvalue === void 0
(local undefined can be modified but the global cannot;void
will always returnundefined
though) - null:
value === null
- null or undefined:
value === null || typeof value === "undefined"
or if we are sure thatundefined
has not been modified thenvalue === null || value === undefined
- Arrays:
Array.isArray(value)
- Instances:
value instanceof SomeInstance
- Plain objects:
Object.getPrototypeOf(value) === Object.prototype