top of page

Introduction to Duck Typing in TypeScript



What is Duck Typing?

Duck Typing is usually used in code that needs to handle a range of different data, often without knowing exactly what parameters will be passed by a caller. Think here of some of the uses you encountered for switch statements or complex if/else blocks. These are typically places where Duck Typing might come in handy or even offer an alternative.


Why Duck Type?

A common pattern for duck typing in dynamic languages is to try and perform an action assuming a given value fits what we expect, and then handle whatever exceptions might arise. Python is a good example here:

from typing import Any

def is_duck(value: Any) ->bool:
    try:
        value.quack()
        return True
    except (Attribute, ValueError):
        return False

This is evidently a silly example, but the point is simple — you get a value, you check if it quacks by calling its .quack() method- if it quacks, return true, if either an attribute or value error is raised, return false.

In Python try-except is an accepted pattern, one that is also used internally by built-ins such as hasattr and throughout the standard library. In JavaScript in contrast, try-catch is more restricted — you can neither define different catch blocks depending on the prototypes of the errors that have been thrown, nor for that matter even be certain that what has been thrown is even an instance of error at all.


You, therefore, have to be more verbose and a lot safer when handling errors, which makes this somewhat of an anti-pattern in JavaScript and TypeScript. The common practice instead is to do something like this:

function isDuck(value){
    return!!(
        value &&
        typeof value === 'object' &&
        typeof Reflect.get(value, 'quack') === 'function'
    );
}

In the above predicate we (1) check that the parameter value is of type “object”, (2) that it is not null because the type of null is “object” in JavaScript (🤦‍♂), and (3) using the Reflect.get method, we retrieve the value for “quack” safely and check that it is indeed a function.


This kind of predicate is probably familiar to most readers — after all, JavaScript code is often filled with boolean checks, be they abstracted into separate functions or simply written inline.


Yet this is where JavaScript and TypeScript differ — the parameter value might be a duck, but neither the IDE nor the JavaScript interpreter know what a duck is. In TypeScript on the other hand, Duck can and will be a type:

interface Duck{
    quack(): string;
}

function isDuck(value: unknown): value is Duck{
    return!!(
        value && 
        typeof value === 'object' &&
        typeof Reflect.get(value, 'quack') === 'function'
    );
}

Notice the is keyword used in the return value typing of isDuck, this is what’s called a type predicate in TypeScript, and it’s one of the nicer features of the language: A type predicate is a function returning a boolean value that acts as a custom type guard; in effect telling the TypeScript compiler that a given value is of a given type. That is, in the above example, if the function isDuck returns true, the compiler will know that the value has the type Duck.


Why is this a big deal? Because our function now has dual utility — it remains a predicate, returning a boolean value, which means we can use it like a predicate in terms of JavaScript, but at the same time, it also affects the TypeScript compiler and thereby the IDE and any other tooling (read ESLint) that might be linked to the compiler.


Example Use Case: recursiveResolve

One handy use for duck typing is when you have code that might accept both Promises and non-Promises. The built-in way to handle this is to use Promise.resolve() to wrap the value, this will either unpack the Promise object if given a Promise object— or wrap the value in a Promise object and then unpack it. The problem with this is that it has a slight overhead — you will need to await resolution even for non-promise values.


An alternative approach would be to “duck type” promises using a type predicate, which by convention would be called isPromise.


Let’s assume we created a custom method to recursively traverse an object, resolving whatever promises might be nested inside it (the code below is adapted from one of my libraries, you can see the original here), which is a good use case for such a type predicate:

function isRecord<T = any>(value: unknown): value is Record<string | symbol, T>{
    return!!(value && typeof value === 'object');
}

function isPromise<T = unknown>(value: unknown): value is Promise<T>
{
    return isRecord(value) && Reflect.has(value, 'then');
}

async function recursiveResolve<T>(
    parsedObject: Record<string,any>,
): Promise<T>{
    const output={};
    for(const [key, value] of Object.entries(parsedObject)){
        const resolved: unknown=isPromise(value) ? await value : value;
        if(isRecord(resolved)){
            Reflect.set(output, key, await recursiveResolve(resolved));
        } else {
            Reflect.set(output, key, resolved);
        }
    }
    return output as T;
}

As you can see we defined two predicates in the above — isPromise and isRecord, both of which accept an optional generic parameter, which makes them reusable. We are then able to use them inside the recursiveResolve function with very little overhead with typing being correctly inferred throughout the function.


Advantages of Duck Typing
  1. It leads to fewer lines of code. This makes it look cleaner; thus making your code easier to read and faster to write.


Disadvantages of Duck Typing
  1. Poorly written duck typing code can lead to long and painful hours debugging a program that should never have needed debugging to begin with. This can then Lead to costly development time and more working hours.

  2. With duck typing, errors are discovered during runtime, which makes it harder to go back and fix past mistakes.



Source: Medium - Na'aman Hirschfeld


The Tech Platform

0 comments

Recent Posts

See All

Comments


bottom of page