sindresorhus / ow

Function argument validation for humans
https://sindresorhus.com/ow/
MIT License
3.81k stars 108 forks source link

Add `method` helper #202

Closed mmkal closed 3 years ago

mmkal commented 3 years ago

This is something I've wished existed for a while. The problem being: I'd like to write a function with specific input types, which is going to be invoked from call-sites outside the project I'm working in (e.g., a library function, an http request/serverless function handler/some json file processor etc.).

But I'd also like to be able to call it internally - the simplest use case being from a unit test.

Example:

// calculator.ts
import ow from 'ow'

const add = ow.method([ow.number, ow.number], (a, b) => a + b)

const subtract = ow.method([ow.number, ow.number], (a, b) => a - b)

const multiply = ow.method([ow.number, ow.number], (a, b) => a * b)

const divide = ow.method([ow.number, ow.number.not.equal(0)], (a, b) => a / b)

Note that a and b are strongly typed (as numbers in the above example) in each function.

Previously the options aren't that great. You can either be safe by typing all inputs as unknown, which makes sure that the validation is done, but offers no help to callers or the function:

const add = (a: unknown, b: unknown) => {
  // good: the type system makes sure we remember to validate the inputs (albeit with some boilerplate)
  ow(a, ow.number)
  ow(b, ow.number)
  return a + b
}

// bad: the type system doesn't stop callers from sending bad inputs.
// This isn't a bug in the function, but it'll still lead to errors:
add('foo', { bar: true })

Or you can type them as number, which provides types to callers, but means that allows skipping the assertion at the type level, leading to errors at runtime if bad data is passed to the function:

// bad: type system has been told the inputs will definitely be a number. But what if the inputs come from an `any` type, or javascript?
const add = (a: number, b: number) => a + b

app.use((req, res) => {
  // undefined behaviour if someone sends a body like { "a": "foo", "b": { "bar": true } }?
  res.send({ sum: add(req.body.a, req.body.b) })
})

Or, you can write a separate type signature, leading to confusing/unnecessary boilerplate:

Instead of:

const add = ow.method([ow.number, ow.number], (a, b) => a + b)

you would have to do:

const add: (a: number, b: number) => number = (a: unknown, b: unknown) => {
  ow(a, ow.number)
  ow(b, ow.number)
  return a + b
}

Curious to hear any thoughts on the idea!

Update :I've since noticed similar functionality in zod.

sindresorhus commented 3 years ago

Looks like a useful utility to me.

sindresorhus commented 3 years ago

What do you think about the name .wrap instead of .method?

sindresorhus commented 3 years ago

Bump :)

mmkal commented 3 years ago

@sindresorhus sorry, I don't think I'll find the time anytime soon to take this over the line, so closing. Feel free to use as a reference for a future implementation though/if you want to make an issue for this idea.