Function Argument
A function argument is a value passed to a function when it is called. Arguments are used to provide input to the function so that it can perform its task. Functions can have zero or more arguments, and the number of arguments a function can accept is determined by its signature.
Let's see step by step how to implement the Function Argument pattern.
Example 1: Fetcher
In this example, we simply want to fetch data from a URL using the fetch
API.
const result = await fetch("http://localhost:3000/api/users", {
method: "GET",
});
Wrapping with a Function
When we want to make it more reusable, we can create a function that accepts the URL and the method as arguments.
// Define the fetcher options
interface FetcherOptions {
baseUrl: string;
path?: string;
method?: "GET" | "POST" | "PUT" | "DELETE";
}
// Define the fetcher function
function fetcher(options: FetcherOptions) {
return fetch(`${options.baseUrl}${options.path}`, {
method: options.method ?? "GET",
});
}
// Usage
await fetcher({ baseUrl: "http://localhost:3000", path: "/api/users" });
In this example, we created a fetcher
function that accepts an object with the baseUrl
, path
, and method
properties. This function is more reusable and can be used to fetch data from different URLs.
Using a Function Argument
However, when we want to call fetcher
, we need to passing the baseUrl
in every call. To make it more convenient, we can create a function that returns the fetcher
function result with the baseUrl
already set.
/// <reference types="node" />
// Define the fetcher options
type FetcherOptionFunc<Context extends object = {}> = (context: Context) => {
path?: string;
method?: "GET" | "POST" | "PUT" | "DELETE";
logMessage?: string;
};
type Env = "dev" | "prod";
type EnvOptions = {
activeEnv: Env;
env: Record<
Env,
{
baseUrl: string;
}
>;
};
const context: EnvOptions = {
activeEnv: "dev",
env: {
dev: {
baseUrl: "http://localhost:3000",
},
prod: {
baseUrl: "https://api.example.com",
},
},
};
function fetcher(optionFunction: FetcherOptionFunc<EnvOptions>) {
const options = optionFunction(context);
if (options.logMessage) console.log(options.logMessage);
const baseUrl = context.env[context.activeEnv].baseUrl;
return fetch(`${baseUrl}${options.path}`, {
method: options.method ?? "GET",
});
}
await fetcher((c) => ({
path: "/api/users",
logMessage:
`[Env=${c.activeEnv}] Fetching users from ${c.env[c.activeEnv].baseUrl}`,
}));
In this example, we created a fetcher
function that accepts a function that returns an object with the baseUrl
, path
, method
, and logMessage
properties. This function is more flexible and allows us to set the baseUrl
based on the environment. We can also add a logMessage
property to log a message before fetching the data.
Decoupling the Function Logic
By using the Function Argument pattern, we can create more flexible and reusable functions that accept different arguments and configurations. This pattern allows us to decouple the function logic from the input values, making the function more versatile and easier to maintain.
/// <reference types="node" />
/**
* Define the fetcher options
*/
type FetcherOptionFunc<Context extends object = {}> = (context: Context) => {
path?: string;
method?: "GET" | "POST" | "PUT" | "DELETE";
logMessage?: string;
};
type EnvBase = {
activeEnv: string,
env: Record<string, { baseUrl: string }>,
}
/**
* Create a fetcher function with the context
*/
function createFetcher<Context extends EnvBase>(context: Context) {
return function fetcher(optionFunction: FetcherOptionFunc<Context>) {
const options = optionFunction(context);
if (options.logMessage) console.log(options.logMessage);
const baseUrl = context.env[context.activeEnv].baseUrl;
return fetch(`${baseUrl}${options.path}`, {
method: options.method ?? "GET",
});
};
}
/**
* Usage
*/
type Env = "dev" | "prod";
type EnvOptions = {
activeEnv: Env;
env: Record<
Env,
{
baseUrl: string;
}
>;
};
const fetcher = createFetcher<EnvOptions>({
activeEnv: process.env.NODE_ENV === "production" ? "prod" : "dev",
env: {
dev: {
baseUrl: "http://localhost:3000",
},
prod: {
baseUrl: "https://api.example.com",
},
},
});
await fetcher((c) => ({
path: "/api/users",
logMessage:
`[Env=${c.activeEnv}] Fetching users from ${c.env[c.activeEnv].baseUrl}`,
}));
In this example, we created a createFetcher
function that returns a fetcher
function with the context already set. This allows us to create a fetcher
function with the environment configuration and reuse it multiple times without passing the context every time.
Real World Examples
Example 1: OData Query Builder
OData is a standard protocol for querying and updating data. It defines a set of query options that clients can use to request data from a server. The OData query builder is a library that helps you build OData queries in a type-safe way.
import { ODataExpression} from 'ts-odata-client';
interface User {
firstName: string;
lastName: string;
}
const expression = ODataExpression
.forV4<User>()
.filter((p) =>
p.firstName.$equals("john")
);
console.log(`Query String: ${expression.build().filter}`);
// Output: Query String: $filter=firstName eq 'john'
For this example, we are using the ts-odata-client
library to build an OData query. The ODataExpression
class provides a fluent API for building OData queries. In this example, we are filtering the users whose firstName
is equal to "john".
How Function Argument Works
Focus on the filter
function argument, you can see that the filter
function accepts a predicate function that takes a parameter p
. The p
parameter is an object that represents the properties of the User
interface. You can access the properties of the User
interface, which is firstName
and lastName
, and use them to build the filter expression.
Without the Function Argument pattern, you would have to pass the User
object directly to the filter
function, which would make the code less readable and harder to maintain.
import { ODataExpression} from 'ts-odata-client';
interface User {
firstName: string;
lastName: string;
}
const expression = ODataExpression
.forV4<User>()
.filter((p) => {
type FilterFunction = typeof p;
return p.firstName.$equals("john")
});
ODataExpression Implementation
From source code ts-odata-client@2.0.2, you can see how the filter
function is implemented using the Function Argument pattern.
// File: src/lib/ODataQueryBase.ts
export class ODataQueryBase<T, U = ExcludeProperties<T, unknown[]>> {
// ...
public filter(
predicate:
| BooleanPredicateBuilder<T>
| ((builder: EntityProxy<T, true>, functions: FilterAccessoryFunctions<T>) => BooleanPredicateBuilder<T>),
) {
if (typeof predicate === "function")
predicate = predicate(
this.provider[createProxiedEntity]() as unknown as EntityProxy<T, true>,
new FilterAccessoryFunctions<T>(),
);
const expression = new Expression(ExpressionOperator.Predicate, [predicate], this.expression);
return this.provider.createQuery<T, U>(expression);
}
// ...
}
Example Projects
- ts-odata-client - A TypeScript library for building OData queries.