Skip to content

Instantly share code, notes, and snippets.

@sam-goodwin
Created January 11, 2024 22:55
Show Gist options
  • Save sam-goodwin/467d5226c62d8710e5202eab6d4a75f4 to your computer and use it in GitHub Desktop.
Save sam-goodwin/467d5226c62d8710e5202eab6d4a75f4 to your computer and use it in GitHub Desktop.
function declaration decorator use-cases

Decorators on functions would be useful for frameworks that do basic reflection to infer configuration and bundle applications such as APIs, cloud functions, etc.

Examples

I think a good place to look at for examples is the Python ecosystem:

  1. Chalice uses decorators for wiring up function handlers to AWS resources
  2. Modal uses decorators to provision infrastructure and model the boundaries compute (different hosted functions)

Example 1: an @function() annotation marks a function as a hosted FaaS Function

@lambda()
async function myJob(): Promise<string> {
  // does some work in a remote server somewhere
  return "some result"
}

Example 2: a @schedule decorator marks a function as running on a schedule

@schedule.every(1, "day")
export async function myJob() { .. }

Example 3: an @api decorator marks a function as an API endpoint

@api(path="/my/api", method="GET")
@use(oAuth) // with middleware
export async function myApi(req) { .. }

Example 4: wrap a function in a retry handler

@retry(maxAttempts=3)
export async function myJob(): Promise<string> { .. }

Example 5: register a function as a consumer of a Queue

const queue = new Queue<string>("my-queue");

@queue.consumer()
export async function consume(message) {
  message.content; // string
}

Comparison with current approaches

1. File and Export naming conventions

Need to be learned, are not type-safe.

// api/my/api/route.ts
export async function GET(req: NextRequest): Promise<NextResponse> { .. }

2. Builders/helpers - this is type safe but verbose

Are type-safe, but are verbose especially when chaining and cannot be hoisted

export const myJob = framework.function({ 
  path
  method: "GET"
}, async (request) => {
  ..
})

3. Functional chains

This is pretty standard practice:

export const myConsumer = queue.consume(message => {
  //
})

But it's also very tempting to do this without exporting a function that can be bundled:

queue.consume(message => {
  //
})

Type Level Features

If TS support decorators, I think it's important for two type-level features to exist:

1. allow the decorator to return a new function type

E.g. going back to the myJob lambda Function, I want the decorator to augment the type so that i can extend its interface

@lambda()
async function myJob(): Promise<string> {
  // does some work in a remote server somewhere
  return "some result"
}

This can be called in 1 of 2 ways:

  1. synchronously call it and wait for the result
const result = await myJob()
  1. "spawn" as an asynchronous job with a handle to that job
const resultHandle = await myJob.spawn()

await resultHandle.checkResult();

2. allow type arguments of decorated function to be inferred

// my decorator
function s3EventHandler() {
  return function decorator(func: (event: S3Event) => { .. })
}

@s3EventHandler()
async function handler(event) {
  // event is inferred to be of type S3Event
  // i shouldn't have to add an explicit type
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment