Write 'once' function with TS from scratch
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