Skip to content

feature request: Change function signature of safeFn #3

@geoffgscott

Description

@geoffgscott

Feels a bit weird to have to return a Result from the callback. My more expected use case would be pre-wrapping functions (possibly on export?) so that I don't need to wrap them in fromUnsafe everywhere I consume.

I would also like the ability to explicitly type Data and Err when creating a safeFn; with the current signature I have to input args as well which IMO is best to always be inferred.

const createErrorResult = (e: unknown, errorHandler?: (e: unknown) => any) =>
    ({
        success: false,
        error: errorHandler?.(e) ?? null,
    }) as const

const createSuccessResult = <T>(data: T) =>
    ({
        success: true,
        data,
    }) as const

/**
 * Create a safe function from an unsafe one.
 *
 * Supports two usage patterns:
 * 1. Full inference: `safeFn(callback, errorHandler)` - infers Data, Args, and Err from callback
 * 2. Explicit types: `safeFn<Data, Err>()(callback, errorHandler)` - specify Data/Err, infer Args
 *
 * @example
 * // Pattern 1: Full inference
 * const getName = n.safeFn((first: string, last: string) => {
 *   return { name: `${first} ${last}` };
 * });
 * // getName: (first: string, last: string) => Result<{ name: string }, null>
 *
 * // Pattern 2: Explicit Data/Err types
 * const getUser = n.safeFn<User, string>()((id: string) => {
 *   return db.findUser(id);
 * }, () => "FAILED_TO_GET_USER");
 * // getUser: (id: string) => Result<User, string>
 */

// Overload 1: Direct call with callback - full inference
function safeFn<Args extends unknown[], Data, Err = null>(
    cb: (...args: Args) => Data,
    eh?: (e: unknown) => Err,
): (...args: Args) => SafeReturnType<Data, Err>

// Overload 2: Curried call with explicit types - no arguments returns a builder
function safeFn<Data, Err = null>(): <Args extends unknown[]>(
    cb: (...args: Args) => Data,
    eh?: (e: unknown) => Err,
) => (...args: Args) => SafeReturnType<Data, Err>

// Implementation
function safeFn(
    cb?: (...args: any[]) => any,
    eh?: (e: unknown) => any,
): any {
    const wrapCallback = (
        callback: (...args: any[]) => any,
        errorHandler?: (e: unknown) => any,
    ) => {
        return (...args: any[]) => {
            try {
                const result = callback(...args)

                if (result instanceof Promise)
                    return result
                        .then(createSuccessResult)
                        .catch((e: unknown) => createErrorResult(e, errorHandler))

                return createSuccessResult(result)
            } catch (e) {
                return createErrorResult(e, errorHandler)
            }
        }
    }
    // nonNull assertion required because of the weird functional overload
    return wrapCallback(cb!, eh)
}

// Pattern 1: Explicit types with curried call
const _testFn = safeFn<{ name: string }, string>()(
    (first: string, last: string) => {
        return {
            name: `${first} ${last}`,
        }
    },
) satisfies (first: string, last: string) => Result<{ name: string }, string>

// Pattern 2: Full inference with direct call
const _testFn2 = safeFn((first: string, last: string) => {
    return {
        name: `${first} ${last}`,
    }
}) satisfies (first: string, last: string) => Result<{ name: string }, null>

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions