Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 25 additions & 3 deletions src/debounce.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,23 @@ interface DebounceOptions {
}

/**
* @see https://unpkg.com/lodash.debounce@4.0.8/index.js
* @description
* Creates a debounced function that delays invoking `func` until after `waitMilliseconds`
* have elapsed since the last time the debounced function was invoked.
* The debounced function comes with a `cancel` method to cancel delayed `func`
* invocations and a `flush` method to immediately invoke them. Provide `options`
* to indicate whether `func` should be invoked on the leading and/or trailing edge of
* the `waitMilliseconds` timeout. The `func` is invoked with the last arguments provided to the
* debounced function. Subsequent calls to the debounced function return the result of
* the last `func` invocation.
*
* @param {Function} func - The function to debounce.
* @param {number} [waitMilliseconds=0] - The number of milliseconds to delay.
* @param {Object} [options={}] - The options object.
* @param {boolean} [options.leading=false] - Specify invoking on the leading edge of the timeout.
* @param {number} [options.maxWait] - The maximum time `func` is allowed to be delayed before it's invoked.
* @param {boolean} [options.trailing=true] - Specify invoking on the trailing edge of the timeout.
* @returns {{debounce: Function, cancel: Function, flush: Function}} An object containing the debounced function, a cancel function, and a flush function.
*/
export function debounce<Args extends unknown[]>(
func: (...args: Args) => unknown,
Comment on lines +24 to 27
Copy link
Contributor

@yujeong-jeon yujeong-jeon Jun 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not related to the changes in this PR, but I have a question regarding the debounce implementation, especially about the return type of func.

Currently, the func argument is typed to return unknown. Is this considered type-safe in TypeScript?
For example, if I pass an async function that returns a Promise<boolean> to debounce, the resulting debounced function’s return type becomes unknown, which requires type assertions or runtime checks when using the return value.
Would it be possible or preferable to type the debounced function so that it preserves the original return type of func, instead of returning unknown?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@yujeong-jeon thanks for your comment.

I think it would be better to improve this using generics instead of unknown. I'll handle this in a separate issue.
thanks.

Expand All @@ -30,13 +46,16 @@ export function debounce<Args extends unknown[]>(
return setTimeout(pendingFunc, wait)
}

// Calculate remaining wait time considering maxWait.
const remainingWait = (time: number) => {
const timeSinceLastCall = time - (lastCallTime as number)
const timeSinceLastInvoke = time - lastInvokeTime
const timeWaiting = waitMilliseconds - timeSinceLastCall

return maxWait === undefined ? timeWaiting : Math.min(timeWaiting, maxWait - timeSinceLastInvoke)
}

// Check if it's time to invoke.
const shouldInvoke = (time: number) => {
if (lastCallTime === null) {
return true
Expand All @@ -45,13 +64,13 @@ export function debounce<Args extends unknown[]>(
const timeSinceLastInvoke = time - lastInvokeTime

return (
lastCallTime === null ||
timeSinceLastCall >= waitMilliseconds ||
timeSinceLastCall < 0 ||
timeSinceLastCall < 0 || // Handle system time changes.
(maxWait !== undefined && timeSinceLastInvoke >= maxWait)
)
}

// Handle trailing edge invocation.
const trailingEdge = (time: number) => {
timeoutId = null
if (trailing && lastArgs) {
Expand All @@ -61,14 +80,17 @@ export function debounce<Args extends unknown[]>(
return result
}

// Function called when timer expires.
const timerExpired = () => {
const time = Date.now()
if (shouldInvoke(time)) {
return trailingEdge(time)
}
// Restart timer with remaining wait time.
timeoutId = startTimer(timerExpired, remainingWait(time))
}

// Handle leading edge invocation.
const leadingEdge = (time: number) => {
lastInvokeTime = time
timeoutId = startTimer(timerExpired, waitMilliseconds)
Expand Down
24 changes: 23 additions & 1 deletion src/delay.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,32 @@
import toNumber from './toNumber'

/**
* @description Invokes `func` after `wait` milliseconds. Any additional arguments are provided to `func` when it's invoked.
* @category Function
* @param {(...args: T) => void} func The function to delay.
* @param {number} wait The number of milliseconds to delay invocation.
* @param {...T} args The arguments to invoke `func` with.
* @returns {number} Returns the timer ID.
* @example
*
* delay((text) => {
* console.log(text);
* }, 1000, 'later');
* => Logs 'later' after 1 second.
*/
export function delay<T extends unknown[]>(func: (...args: T) => void, wait: number, ...args: T): number {
if (typeof func !== 'function') {
throw new TypeError('Expected a function')
}
const timeout = setTimeout(() => func(...args), wait)
// Use `setTimeout` directly as it's the core of the delay functionality.
const timeout = setTimeout(() => func(...args), +wait || 0)

// In Node.js, setTimeout returns a Timeout object, while in browsers it returns a number.
// We attempt to convert it to a number for consistency, although returning
// the native `setTimeout` return type might be more flexible.
// However, lodash returns a number, so we follow that for compatibility.
// If `toNumber` is not strictly necessary or if the environment guarantees a number,
// this could be simplified. Given we have `toNumber`, we use it.
return toNumber(timeout)
}

Expand Down
24 changes: 21 additions & 3 deletions src/difference.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,39 @@
/**
* @description
* Creates an array of `array` values not included in the other given arrays.
* This implementation utilizes a `Set` for improved performance when checking for exclusions,
* offering O(1) average time complexity for lookups.
*
* @param {ArrayLike<T> | null | undefined} array - The array to inspect.
* @param {...ArrayLike<T>[]} values - The values to exclude.
* @returns {T[]} Returns the new array of filtered values.
*/
export function difference<T>(array: ArrayLike<T> | null | undefined, ...values: ArrayLike<T>[]): T[] {
// Return an empty array if the input array is null, undefined, or empty.
if (!array || array.length === 0) {
return []
}

const toExclude = new Set<T>()
const valueLength = values.length

// Populate the Set with all values to be excluded.
// This allows for efficient O(1) average time complexity checks later.
for (let i = 0; i < valueLength; i++) {
const val = values[i]
const valLength = val.length
for (let j = 0; j < valLength; j++) {
toExclude.add(val[j])
if (val) {
const valLength = val.length
for (let j = 0; j < valLength; j++) {
toExclude.add(val[j])
}
}
}

const result: T[] = []
// Iterate through the input array.
for (let i = 0; i < array.length; i++) {
const element = array[i]
// If the element is not in the exclusion Set, add it to the result.
if (!toExclude.has(element)) {
result.push(element)
}
Expand Down