Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More β†’
Socket
Sign inDemoInstall
Socket

@cloudflare/util-en-garde

Package Overview
Dependencies
Maintainers
31
Versions
32
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@cloudflare/util-en-garde - npm Package Compare versions

Comparing version 7.0.2 to 7.0.3

8

CHANGELOG.md

@@ -6,2 +6,10 @@ # Change Log

## [7.0.3](http://stash.cfops.it:7999/fe/stratus/compare/@cloudflare/util-en-garde@7.0.2...@cloudflare/util-en-garde@7.0.3) (2020-03-11)
**Note:** Version bump only for package @cloudflare/util-en-garde
## [7.0.2](http://stash.cfops.it:7999/fe/stratus/compare/@cloudflare/util-en-garde@7.0.0...@cloudflare/util-en-garde@7.0.2) (2020-02-12)

@@ -8,0 +16,0 @@

4

package.json
{
"name": "@cloudflare/util-en-garde",
"description": "",
"version": "7.0.2",
"version": "7.0.3",
"types": "./dist/index.d.ts",

@@ -29,3 +29,3 @@ "main": "lib/index.js",

},
"gitHead": "3b549c3124e3c818ddbc6c05d7c6be3d4171e15b"
"gitHead": "c243531c9bf0a4bdbb6423b3d71c4a783808e40a"
}
# En Garde! 🀺
Safely and declaratively create user-defined type guards
Declare your types with codecs and keep the type definitions in sync with the codec.
```ts
// person.ts
import axios from 'axios';
import { eg, TypeFromCodec } from '@cloudflare/util-en-garde';
// Declare Person codec
const Person = eg.object({
firstName: eg.string,
lastName: eg.string.optional,
address: eg.object({
street: eg.string,
city: eg.string,
state: eg.string,
zip: eg.number
}),
nicknames: eg.array(eg.string).optional,
favoritePrimaryColor: eg.union([
eg.literal('red'),
eg.literal('blue'),
eg.literal('yellow')
])
});
/**
* Derive Person type information from codec:
*
* {
* firstName: string
* lastName?: string | undefined
* address: {
* street: string
* city: string
* state: string
* zip: number
* }
* nicknames?: string[] | undefined
* favoritePrimaryColor: "red" | "blue" | "yellow"
* }
*/
export type Person = TypeFromCodec<typeof Person>;
/** πŸ‘†
* typeof is accessing the (value) Person codec's type
* information and passes it into the TypeFromCodec type
* helper
*/
// handler accepts the Person *type*
const handler = (p: Person) => {
console.log(p.firstName.toUpperCase());
};
axios
.get('https://www.example.com')
.then(res => res.data)
// Here we are using the Person *codec* as a value and
// decoding
.then(Person.assertDecode)
.then(handler);
```
## Decoding IO data
TypeScript has done a lot to bring type safety to the JS ecosystem, but decoding
data from IO is an area that is still largely overlooked even in TypeScript. For
example, `axios`'s type-definitions allow you to specify a type that you expect
the data to be. This is still a great improvement over untyped JS since you can
change the type in one place and immediately know what other places in your code
need to be updated, but this still does not provide any kind of runtime
validation.
```ts
// unsafe.ts
import axios from 'axios';
axios
.get<string>('https://www.example.com')
.then(res => console.log(res.data.toUpperCase()));
```
When we tell the type checker the response is going to be of type `string`, we
can still get unexpected runtime errors. What happens if this API doesn't send
back the response shape we're expecting? TypeScript will happily pretend that
it's going to, but this is not safe!
TypeScript actually has a more appropriate type for this β€”Β the `unknown` type!
```ts
// safe_unknown.ts
import axios from 'axios';
axios.get<unknown>('https://www.example.com').then(res => {
if (typeof res.data === 'string') {
console.log(res.data.toUpperCase());
} else {
throw new Error('response was not of type string');
}
});
```
Unfortunately, the `unknown` type can be cumbersome to handle directly. The type
checker requires you to perform runtime checks in order to narrow the type to be
usable for just about anything. This _is_ actually type safe, though!
Fortunately there are libraries that are built for this! `io-ts` is one such
library that is built on some really solid ideas, though the API leaves some
things to be desired. We've created a lightweight wrapper around it called
`util-en-garde` to provide a nicer API. If you're interested in the reasons,
please check out the
[source code for our wrapper](https://bitbucket.cfdata.org/projects/FE/repos/stratus/browse/src/common/util/util-en-garde/src/index.ts)!
```ts
// safe_decode.ts
import axios from 'axios';
import { eg } from '@cloudflare/util-en-garde';
axios
.get<unknown>('https://www.example.com')
.then(res => res.data)
.then(eg.string.assertDecode)
// will never hit code path below if the response was wrong
// and str is of type string because that is the type assert
// returns on eg.string
.then(str => console.log(str.toUpperCase()));
```
The wrapper provides an `assertDecode` method that _will throw an error_ if it
fails to decode containing detailed information about _why_ it failed to decode.
With this, you can safely and confidently handle unknown data at the IO
boundaries of your application and know with confidence that the codepaths
deeper in your codebase _will_ have the right data flowing through them!
It provides a declarative API for data structures. For example, we can create
construct a codec for a person object.
```ts
// person.ts;
import { eg, TypeFromCodec } from '@cloudflare/util-en-garde';
// Declare Person codec
const Person = eg.object({
firstName: eg.string,
lastName: eg.string.optional,
address: eg.object({
street: eg.string,
city: eg.string,
state: eg.string,
zip: eg.number
}),
nicknames: eg.array(eg.string).optional,
favoritePrimaryColor: eg.union([
eg.literal('red'),
eg.literal('blue'),
eg.literal('yellow')
])
});
```
Additionally, types can be derived from the codec. This means we can have the
codec be the single source of truth. If the codec changes, the type information
changes with it. If the type needs to change, you change it by changing the
codec. This guarantees that our runtime checks are consistent with the types we
use elsewhere in our application! Types and values exist in separate namespaces,
so we can actually use the same name for both the codec and the type!
```ts
// person.ts;
```
Now we can write some pretty neat strongly typed code!
```ts
// person.ts
import axios from 'axios';
import { eg, TypeFromCodec } from '@cloudflare/util-en-garde';
// Declare Person codec
const Person = eg.object({
firstName: eg.string,
lastName: eg.string.optional,
address: eg.object({
street: eg.string,
city: eg.string,
state: eg.string,
zip: eg.number
}),
nicknames: eg.array(eg.string).optional,
favoritePrimaryColor: eg.union([
eg.literal('red'),
eg.literal('blue'),
eg.literal('yellow')
])
});
/**
* Derive Person type information from codec:
*
* {
* firstName: string
* lastName?: string | undefined
* address: {
* street: string
* city: string
* state: string
* zip: number
* }
* nicknames?: string[] | undefined
* favoritePrimaryColor: "red" | "blue" | "yellow"
* }
*/
export type Person = TypeFromCodec<typeof Person>;
/** πŸ‘†
* typeof is accessing the (value) Person codec's type
* information and passes it into the TypeFromCodec type
* helper
*/
// handler accepts the Person *type*
const handler = (p: Person) => {
console.log(p.firstName.toUpperCase());
};
axios
.get('https://www.example.com')
.then(res => res.data)
// Here we are using the Person *codec* as a value and
// decoding
.then(Person.assertDecode)
.then(handler);
```
## Handling Errors
`assertDecode` _does_ throw an error if it fails to decode, so we need to handle
that. We have some utilities in dash's `common/utils/decode` that will decode
the data, and if decoding fails we will log an error to Sentry to make us aware
that an API is sending us back an unexpected response.
`decode` is built to be used with fetch:
```ts
// handling_fetch_errors.ts
import { eg, TypeFromCodec } from '@cloudflare/util-en-garde';
import { decode } from 'common/utils/decode';
const Person = eg.object({
firstName: eg.string,
lastName: eg.string
});
fetch('api.cloudflare.com')
// if decoding fails, we will log an error to Sentry
// with detailed information about what failed to decode
// before re-throwing the error so typically you should
// not use assertDecode directly
.then(decode(Person))
.then(p => console.log(p.firstName))
.catch(err => {
// we might get a decoding error in here now
});
```
Assuming you're using a `v4` API with `util-http` you should use
`httpUtilDecode`:
```ts
// handling_http_util_errors.ts
import { eg, TypeFromCodec } from '@cloudflare/util-en-garde';
import { httpUtilDecode, resultOf, getResult } from 'common/utils/decode';
const Person = eg.object({
firstName: eg.string,
lastName: eg.string
});
fetch('api.cloudflare.com/v4/person')
// httpUtilDecode expects the response of util-http
// resultOf wraps the codec with response object
.then(httpUtilDecode(resultOf(Person)))
// getResult plucks the result off the response object
.then(getResult)
.then(p => console.log(p.firstName))
.catch(err => {
// we might get a decoding error in here now
});
```
## Writing custom codecs
This is where those really solid ideas that `io-ts` is built on really shine.
The `io-ts` docs include
[this `DateFromString` example](https://github.com/gcanti/io-ts#custom-types).
Since it was built to be used with `fp-ts` (a library for functional programming
in TypeScript) the `validate` function (3rd argument in the constructor) needs
to return an `Either` type which is a simple discriminated union that indicates
success (right) or failure (left).
```ts
// io-ts_custom_codec.ts
import * as t from 'io-ts';
import { either } from 'fp-ts/lib/Either';
// represents a Date from an ISO string
const DateFromString = new t.Type<Date, string, unknown>(
'DateFromString',
(u): u is Date => u instanceof Date,
(u, c) =>
either.chain(t.string.validate(u, c), s => {
const d = new Date(s);
return isNaN(d.getTime()) ? t.failure(u, c) : t.success(d);
}),
a => a.toISOString()
);
const s = new Date(1973, 10, 30).toISOString();
DateFromString.decode(s);
// right(new Date('1973-11-29T23:00:00.000Z'))
DateFromString.decode('foo');
// left(errors...)
```
To de-mystify what's going on here, we can re-write this and avoid using
`either.chain`, `t.success`, and `t.failure`. Let's also give these one letter
variables some more meaningful names.
Note that the `context` object only gets passed along in the error cases. It
contains path information and other metadata in the event of a failure. Failurs
get returned in an array because some data structures (objects/arrays) may
contain multiple errors.
```ts
// date_from_string.ts
import * as t from 'io-ts';
const DateFromString = new t.Type<Date, string, unknown>(
// First argument is the name of the codec
'DateFromString',
// Second argument is a guard for the decoded type
(value: unknown): value is Date => value instanceof Date,
// Third argument is the "validate" function that attempts to
// turn an unknown value into a decoded value and must return
// an Either discriminated union
(value: unknown, context: t.Context) => {
// first we make sure the value of type unknown is a string
if (typeof value !== 'string')
// return a "Left" error object if value isn't string
return { _tag: 'Left', left: [{ value, context }] };
// create a date object
const date = new Date(value);
// check to see if date is valid
return isNaN(date.getTime())
? // error case since it wasn't a valid date
{ _tag: 'Left', left: [{ value, context }] }
: // success case returns a "Right" with the date object!
{
_tag: 'Right',
right: date
};
},
// Fourth argument is the "encode" function that goes from
// a date object back to a string
(a: Date) => a.toISOString()
);
```
Now that we have a custom codec for `io-ts`, we can simply wrap this up with
`util-en-garde`'s `Codec` class that provides the additional nice API.
```ts
// date_from_string.ts
import { t, Codec, TypeFromCodec } from '@cloudflare/util-en-garde';
// ☝️ util-en-garde re-exports io-ts
// Pass the custom io-ts codec definition into the Codec wrapper
export const DateFromString = new Codec(
new t.Type<Date, string, unknown>(
'DateFromString',
(value: unknown): value is Date => value instanceof Date,
(value: unknown, context: t.Context) => {
if (typeof value !== 'string')
return { _tag: 'Left', left: [{ value, context }] };
const date = new Date(value);
return isNaN(date.getTime())
? { _tag: 'Left', left: [{ value, context }] }
: {
_tag: 'Right',
right: date
};
},
(a: Date) => a.toISOString()
)
);
// It can now be used like any other wrapped codec!
const Person = eg.object({
firstName: eg.string,
birthday: DateFromString.optional
});
/**
* { firstName: string, birthday?: Date | undefined }
*/
type Person = TypeFromCodec<typeof Person>;
// Succeeds because birthday is undefined and optional
const successOne = Person.assertDecode({ firstName: 'jane' });
// Succeeds because birthday is a valid date string
const successTwo = Person.assertDecode({
firstName: 'joe',
birthday: new Date().toISOString()
});
// Throws because birthday is not a valid date string
const failureOne = Person.assertDecode({
firstName: 'jane',
birthday: 'hi'
});
// Throws because birthday is not a string
const failureTwo = Person.assertDecode({
firstName: 'joe',
birthday: false
});
// { firstName: string, birthday?: string | undefined }
const encodedPerson = Person.encode({
firstName: 'jane',
// must pass in Date object here
birthday: new Date()
});
```
Lastly, note that `birthday` on `encodedPerson` is of type `string | undefined`
since `DateFromString`'s "encoded" type is `string`.
Safely and declaratively definte types and runtime encoding/decoding tools in one place!
---

@@ -6,0 +455,0 @@

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚑️ by Socket Inc