Skip to content

Commit

Permalink
Merge pull request #90 from remnantkevin/add-non-strict-modifier
Browse files Browse the repository at this point in the history
Add `nonStrict` modifier for `record` runtypes
  • Loading branch information
hoeck authored Feb 28, 2023
2 parents 8bcf08e + 94a6645 commit e447a3b
Show file tree
Hide file tree
Showing 12 changed files with 338 additions and 75 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
### Unreleased

- add `nonStrict` combinator ([#90](https://github.com/hoeck/simple-runtypes/pull/90))
- `nonStrict` creates a non-strict `record` runtype from a provided `record` runtype
- a non-strict `record` runtype will ignore keys that are not specified in the original runtype's typemap
- non-strict `record` runtypes will replace `sloppyRecord` runtypes

### 7.1.3

- fix: invalid key message was displaying the object instead of the keys, see [#91](https://github.com/hoeck/simple-runtypes/issues/91)
Expand Down
119 changes: 65 additions & 54 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,11 @@ That's how runtypes work.
- [Documentation](#documentation)
* [Intro](#intro)
* [Usage Examples](#usage-examples)
+ [Nesting](#nesting)
+ [Strict Property Checks](#strict-property-checks)
+ [Ignore Individual Properties](#ignore-individual-properties)
+ [Optional Properties](#optional-properties)
+ [Nesting](#nesting)
+ [Non-strict Property Checks](#non-strict-property-checks)
+ [Discriminating Unions](#discriminating-unions)
+ [Custom Runtypes](#custom-runtypes)
* [Reference](#reference)
Expand All @@ -30,7 +32,13 @@ That's how runtypes work.

## Install

`npm install simple-runtypes` or `yarn add simple-runtypes`
```sh
# npm
npm install simple-runtypes

# yarn
yarn add simple-runtypes
```

## Example

Expand All @@ -40,39 +48,39 @@ That's how runtypes work.
import * as st from 'simple-runtypes'

const userRuntype = st.record({
id: st.integer(),
name: st.string(),
email: st.optional(st.string()),
id: st.integer(),
name: st.string(),
email: st.optional(st.string()),
})
```

now, `ReturnType<typeof userRuntype>` is equivalent to

```typescript
interface {
id: number,
name: string,
email?: string
id: number
name: string
email?: string
}
```

2. Use the runtype to validate untrusted data

```typescript
userRuntype({id: 1, name: 'matt'})
userRuntype({ id: 1, name: 'matt' })
// => {id: 1, name: 'matt'}

userRuntype({id: 1, name: 'matt', isAdmin: true})
userRuntype({ id: 1, name: 'matt', isAdmin: true })
// throws an st.RuntypeError: "invalid field 'isAdmin' in data"
```

Invoke a runtype with [`use`](src/custom.ts#L51) to get a plain value back instead of throwing errors:

```typescript
st.use(userRuntype, {id: 1, name: 'matt'})
st.use(userRuntype, { id: 1, name: 'matt' })
// => {ok: true, result: {id: 1, name: 'matt'}}

st.use(userRuntype, {id: 1, name: 'matt', isAdmin: true})
st.use(userRuntype, { id: 1, name: 'matt', isAdmin: true })
// => {ok: false, error: FAIL}

st.getFormattedError(FAIL)
Expand All @@ -86,10 +94,10 @@ Throwing errors and catching them outside is more convenient.

Why should I use this over the plethora of [other](https://github.com/moltar/typescript-runtime-type-benchmarks#packages-compared) runtype validation libraries available?

1. Strict: by default safe against proto injection attacks and unwanted properties
2. Fast: check the [benchmark](https://github.com/moltar/typescript-runtime-type-benchmarks)
3. Friendly: no use of `eval`, a small [footprint](https://bundlephobia.com/result?p=simple-runtypes) and no dependencies
4. Flexible: optionally modify the data while it's being checked: trim strings, convert numbers, parse dates
1. **Strict**: by default safe against `__proto__` injection attacks and unwanted properties
2. **Fast**: check the [benchmark](https://github.com/moltar/typescript-runtime-type-benchmarks)
3. **Friendly**: no use of `eval`, and a small [footprint](https://bundlephobia.com/result?p=simple-runtypes) with no dependencies
4. **Flexible**: optionally modify the data while it's being checked - trim strings, convert numbers, parse dates

## Benchmarks

Expand All @@ -110,7 +118,7 @@ A [`Runtype`](src/runtype.ts#L106) is a function that:

```typescript
interface Runtype<T> {
(v: unknown) => T
(v: unknown) => T
}
```

Expand All @@ -120,62 +128,66 @@ Check the factory functions documentation for more details.

### Usage Examples

#### Strict Property Checks
#### Nesting

When using [`record`](src/record.ts#L134), any properties which are not defined in the runtype will cause the runtype to fail:
Collection runtypes such as [`record`](src/record.ts#L141), [`array`](src/array.ts#L28), and [`tuple`](src/tuple.ts#L42) take runtypes as their parameters:

```typescript
const strict = st.record({name: st.string()})
const nestedRuntype = st.record({
name: st.string(),
items: st.array(st.record({ id: st.integer, label: st.string() })),
})

strict({name: 'foo', other: 123})
// => RuntypeError: Unknown attribute 'other'
nestedRuntype({
name: 'foo',
items: [{ id: 3, label: 'bar' }],
}) // => returns the same data
```

To ignore single properties, use [`ignore`](src/ignore.ts#L6), [`unknown`](src/unknown.ts#L6) or [`any`](src/any.ts#L6):
#### Strict Property Checks

When using [`record`](src/record.ts#L141), any properties which are not defined in the runtype will cause the runtype to fail:

```typescript
const strict = st.record({name: st.string(), other: st.ignore()})
const strict = st.record({ name: st.string() })

strict({name: 'foo', other: 123})
// => {name: foo, other: undefined}
strict({ name: 'foo', other: 123 })
// => RuntypeError: Unknown attribute 'other'
```

Use [`sloppyRecord`](src/record.ts#L159) to only validate known properties and remove everything else:
Using [`record`](src/record.ts#L141) will keep you safe from any `__proto__` injection or overriding attempts.

#### Ignore Individual Properties

To ignore individual properties, use [`ignore`](src/ignore.ts#L6), [`unknown`](src/unknown.ts#L6) or [`any`](src/any.ts#L6):

```typescript
const sloppy = st.sloppyRecord({name: st.string()})
const strict = st.record({ name: st.string(), other: st.ignore() })

sloppy({name: 'foo', other: 123, bar: []})
// => {name: foo}
strict({ name: 'foo', other: 123 })
// => {name: foo, other: undefined}
```

Using any of [`record`](src/record.ts#L134) or [`sloppyRecord`](src/record.ts#L159) will keep you safe from any `__proto__` injection or overriding attempts.

#### Optional Properties

Use the [`optional`](src/optional.ts#L18) runtype to create [optional properties](https://www.typescriptlang.org/docs/handbook/interfaces.html#optional-properties):

```typescript
const squareConfigRuntype = st.record({
const strict = st.record({
color: st.optional(st.string()),
width?: st.optional(st.number()),
width: st.optional(st.number()),
})
```

#### Nesting
#### Non-strict Property Checks

Collection runtypes such as [`record`](src/record.ts#L134), [`array`](src/array.ts#L28), [`tuple`](src/tuple.ts#L42) take runtypes as their parameters:
Use [`nonStrict`](src/nonStrict.ts#L16) to only validate known properties and remove everything else:

```typescript
const nestedRuntype = st.record({
name: st.string(),
items: st.array(st.record({ id: st.integer, label: st.string() })),
})
const nonStrictRecord = st.nonStrict(st.record({ name: st.string() }))

nestedRuntype({
name: 'foo',
items: [{ id: 3, label: 'bar' }],
}) // => returns the same data
nonStrictRecord({ name: 'foo', other: 123, bar: [] })
// => {name: foo}
```

#### Discriminating Unions
Expand Down Expand Up @@ -219,16 +231,16 @@ Finding the runtype to validate a specific discriminating union with is done eff
Write your own runtypes as plain functions, e.g. if you want to turn a string into a `BigInt`:
```typescript
const bigIntStringRuntype = st.string({match: /^-?[0-9]+n$/})
const bigIntStringRuntype = st.string({ match: /^-?[0-9]+n$/ })

const bigIntRuntype = st.runtype((v) => {
const stringCheck = st.use(bigIntStringRuntype, v)
const stringCheck = st.use(bigIntStringRuntype, v)

if (!stringCheck.ok) {
return stringCheck.error
}
if (!stringCheck.ok) {
return stringCheck.error
}

return BigInt(stringCheck.result.slice(0, -1))
return BigInt(stringCheck.result.slice(0, -1))
})

bigIntRuntype("123n") // => 123n
Expand Down Expand Up @@ -260,9 +272,8 @@ Objects and Array Runtypes:

- [`tuple`](src/tuple.ts#L42)
- [`array`](src/array.ts#L28)
- [`record`](src/record.ts#L134)
- [`record`](src/record.ts#L141)
- [`optional`](src/optional.ts#L18)
- [`sloppyRecord`](src/record.ts#L159)
- [`dictionary`](src/dictionary.ts#L87)

Combinators:
Expand All @@ -272,6 +283,7 @@ Combinators:
- [`omit`](src/omit.ts#L8)
- [`pick`](src/pick.ts#L7)
- [`partial`](src/partial.ts#L10)
- [`nonStrict`](src/nonStrict.ts#L16)
- TODO: `get` - similar to Type[key]

Shortcuts:
Expand All @@ -285,7 +297,6 @@ Shortcuts:
- rename [`stringLiteralUnion`](src/stringLiteralUnion.ts#L6) to `literals` or `literalUnion` and make it work
on all types that [`literal`](src/literal.ts#L10) accepts
- rename record to object: [#69](https://github.com/hoeck/simple-runtypes/issues/69)
- nonStrict modifier instead of sloppy: [#68](https://github.com/hoeck/simple-runtypes/issues/68)
- improve docs:
- *preface*: what is a runtype and why is it useful
- *why*: explain or link to example that shows "strict by default"
Expand All @@ -295,5 +306,5 @@ Shortcuts:
- add small frontend and backend example projects that show how to use `simple-runtypes` in production
- test *all* types with [tsd](https://github.com/SamVerschueren/tsd)
- add more combinators: partial, required, get, ...
- separate `Runtype` and `InternalRuntype` and type runtype internals
- separate [`Runtype`](src/runtype.ts#L106) and [`InternalRuntype`](src/runtype.ts#L171) and type runtype internals
(see [this comment](https://github.com/hoeck/simple-runtypes/pull/73#discussion_r948841977))
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export { dictionary } from './dictionary'

// advanced / utility types
export { intersection } from './intersection'
export { nonStrict } from './nonStrict'
export { omit } from './omit'
export { partial } from './partial'
export { pick } from './pick'
Expand Down
24 changes: 24 additions & 0 deletions src/nonStrict.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { internalRecord } from './record'
import { Runtype, RuntypeUsageError } from './runtype'

/**
* Build a non-strict `record` runtype from the provided `record` runtype.
*
* In contrast to a `record` runtype, a non-strict `record` runtype will ignore
* keys that are not specified in the original runtype's typemap (non-strict checking).
*
* When a non-strict `record` runtype checks an object, it will return a new
* object that contains only the keys specified in the original runtype's typemap.
*
* Non-strict checking only applies to the root typemap. To apply non-strict checking
* to a nested typemap, `nonStrict` needs to be used at each level of the typemap.
*/
export function nonStrict<T>(original: Runtype<T>): Runtype<T> {
const fields = (original as any).fields

if (!fields) {
throw new RuntypeUsageError('expected a record runtype')
}

return internalRecord(fields, true)
}
10 changes: 6 additions & 4 deletions src/omit.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { record } from './record'
import { Runtype, RuntypeUsageError } from './runtype'
import { internalRecord } from './record'
import { Runtype, RuntypeUsageError, isNonStrictRuntype } from './runtype'

/**
* Build a new record runtype that omits some keys from the original.
Expand All @@ -21,6 +21,8 @@ export function omit<T, K extends keyof T>(
delete newRecordFields[k]
})

// TODO: keep 'sloppyness'
return record(newRecordFields) as Runtype<any>
return internalRecord(
newRecordFields,
isNonStrictRuntype(original),
) as Runtype<any>
}
10 changes: 6 additions & 4 deletions src/partial.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { optional } from './optional'
import { record } from './record'
import { Runtype, RuntypeUsageError } from './runtype'
import { internalRecord } from './record'
import { Runtype, RuntypeUsageError, isNonStrictRuntype } from './runtype'

/**
* Build a new record runtype that marks all keys as optional.
Expand All @@ -26,6 +26,8 @@ export function partial<T, K extends keyof T>(
}
}

// TODO: keep 'sloppyness'
return record(newRecordFields) as Runtype<any>
return internalRecord(
newRecordFields,
isNonStrictRuntype(original),
) as Runtype<any>
}
10 changes: 6 additions & 4 deletions src/pick.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { record } from './record'
import { Runtype, RuntypeUsageError } from './runtype'
import { internalRecord } from './record'
import { Runtype, RuntypeUsageError, isNonStrictRuntype } from './runtype'

/**
* Build a new record runtype that contains some keys from the original
Expand All @@ -20,6 +20,8 @@ export function pick<T, K extends keyof T>(
newRecordFields[k] = fields[k]
})

// TODO: keep 'sloppyness'
return record(newRecordFields) as Runtype<any>
return internalRecord(
newRecordFields,
isNonStrictRuntype(original),
) as Runtype<any>
}
Loading

0 comments on commit e447a3b

Please sign in to comment.