Write 'once' function with TS from scratch

·

3 min read

Table of contents

TLDR I know about lodash implementation of once. I know about Parameters and ReturnType generic function in Typescript. I want to reinvent the wheel.

Here is example full example on ts playground Example of code

Once?

Suppose I want to create function which accept another function, and allow internal function execute only once.

// once implementation
const once = (fn) => {
    if (typeof fn !== 'function') throw new Error('Not a function');
    let result;
    let isCalled = false;

    return function () {
        if (isCalled === false) {
            result = fn.apply(this, arguments);
            isCalled = true;
            return result;
        }
        return result;
    }
}
// example func for applying once
const test_func = (h = "hello") => {
    console.log(`heavy app ${h}`);
}
test_func();// heavy app hello
test_func();// heavy app hello
test_func();// heavy app hello
test_func();// heavy app hello
console.log(" ----- wrap with once -----");
const test_func_wrapped_once = once(test_func);

test_func_wrapped_once("Hello world"); // heavy app hello
test_func_wrapped_once("Hello world"); // no output
test_func_wrapped_once("Hello world"); // no output

Nothing impossible. Let's add some typescript

Need more typing

Can we rewrite once with TS ?

// We need to have basic type represent any function
type Func = (...args: any[]) => any;
// => and just copy definition from js
const once = (fn) => {
    if (typeof fn !== 'function') throw new Error('Not a function');
    let result;
    let isCalled = false;

    return function () {
        if (isCalled === false) {
            // @ts-expect-error this has any type
            result = fn.apply(this, arguments);
            isCalled = true;
            return result;
        }
        return result;
    }
}

and TS shows several errors

Try to fix these errors. We need the same contract like origin function. Add generic param T

\=>

type Func = (...args: any[]) => any;
// we allow to pass only Functions
const once = <T extends Func>(fn: T) => {
    if (typeof fn !== 'function') throw new Error('Not a function');
    let result;
    let isCalled = false;

    return function (...args: any[]) {
        if (isCalled === false) {
            result = fn.apply(this, args);
            isCalled = true;
            return result;
        }
        return result;
    }
}

Try to use TS utility functions like ReturnType and Parameters

type Func = (...args: any[]) => any;
const once = <T extends Func>(fn: T) => {
    if (typeof fn !== 'function') throw new Error('Not a function');
    let result: ReturnType<T>;
    let isCalled = false;

    return function (...args: Parameters<T>): ReturnType<T> {
        if (isCalled === false) {
            //@ts-expect-error any this type
            result = fn.apply(this, args);
            isCalled = true;
            return result;
        }
        return result;
    }
}

Much better for me, so TS can recheck for me contract of basic function.

Let's implement own ReturnType and Params. Not hard with infer and ternary expression

type MyReturnType<T extends Func> = T extends (...args: any[]) => infer R ? R : any;
type MyParams<T extends Func> = T extends (...args: infer R) => any ? R : any

Here is example full example on ts playground Example of code