Skip to content

Commit

Permalink
Address PR feedback
Browse files Browse the repository at this point in the history
  • Loading branch information
gcanti committed Nov 5, 2017
1 parent b9ea1f3 commit f60d6d4
Show file tree
Hide file tree
Showing 4 changed files with 47 additions and 21 deletions.
20 changes: 18 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ import * as t from 'io-ts'
| function | `Function` | `t.Function` |
| literal | `'s'` | `t.literal('s')` |
| partial | `Partial<{ name: string }>` | `t.partial({ name: t.string })` |
| readonly | `Readonly<{ name: string }>` | `t.readonly({ name: t.string })` |
| readonly | `Readonly<T>` | `t.readonly(T)` |
| readonly array | `ReadonlyArray<number>` | `t.readonlyArray(t.number)` |
| interface | `interface A { name: string }` | `t.interface({ name: t.string })` or `t.type({ name: t.string })` |
| interface inheritance | `interface B extends A {}` | `t.intersection([ A, t.interface({}) ])` |
Expand All @@ -162,7 +162,7 @@ import * as t from 'io-ts'
| refinement || `t.refinement(A, predicate)` |
| map || `t.map(f, type)` |
| prism || `t.prism(type, getOption)` |
| strict || `t.strict(type)` |
| strict || `t.strict({ name: t.string })` |

# Refinements

Expand All @@ -174,6 +174,22 @@ const Positive = t.refinement(t.number, n => n >= 0, 'Positive')
const Adult = t.refinement(Person, person => person.age >= 18, 'Adult')
```

# Strict interfaces

You can make an interface strict (which means that only the given properties are allowed) using the `strict` combinator

```ts
const Person = t.interface({
name: t.string,
age: t.number
})

const StrictPerson = t.strict(Person.props)

t.validate({ name: 'Giulio', age: 43, surname: 'Canti' }, Person) // ok
t.validate({ name: 'Giulio', age: 43, surname: 'Canti' }, StrictPerson) // fails
```

# Mixing required and optional props

Note. You can mix required and optional props using an intersection
Expand Down
30 changes: 16 additions & 14 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -585,35 +585,37 @@ export class ReadonlyArrayType<RT extends Any> implements Type<ReadonlyArray<Typ
export const readonlyArray = <RT extends Any>(type: RT, name?: string): ReadonlyArrayType<RT> =>
new ReadonlyArrayType(type, name)

export class StrictType<RT extends InterfaceType<any>> implements Type<TypeOf<RT>> {
export class StrictType<P extends Props> implements Type<InterfaceOf<P>> {
readonly _tag: 'StrictType' = 'StrictType'
readonly _A: TypeOf<RT>
readonly validate: Validate<TypeOf<RT>>
constructor(readonly type: RT, readonly name: string = `StrictType<${type.name}>`) {
const len = Object.keys(type.props).length
readonly _A: InterfaceOf<P>
readonly validate: Validate<InterfaceOf<P>>
readonly name: string
constructor(readonly props: P, name?: string) {
const loose = type(props)
this.name = name || `StrictType<${loose.name}>`
const len = Object.keys(props).length
this.validate = (v, c) =>
type.validate(v, c).chain(o => {
const keys = Object.keys(v)
loose.validate(v, c).chain(o => {
const keys = Object.getOwnPropertyNames(o)
if (keys.length !== len) {
const errors: Errors = []
for (let i = 0; i < keys.length; i++) {
const key = keys[i]
if (!type.props.hasOwnProperty(key)) {
if (!props.hasOwnProperty(key)) {
errors.push(getValidationError(o[key], c.concat(getContextEntry(key, never))))
}
}
if (errors.length) {
return failures(errors)
}
return errors.length ? failures(errors) : failure(o, c)
} else {
return success(o)
}
return success(o)
})
}
}

/** Specifies that only the given interface properties are allowed */
export function strict<RT extends InterfaceType<any>>(type: RT, name?: string): StrictType<RT> {
return new StrictType(type, name)
export function strict<P extends Props>(props: P, name?: string): StrictType<P> {
return new StrictType(props, name)
}

export { nullType as null, undefinedType as undefined, arrayType as Array, functionType as Function, type as interface }
14 changes: 11 additions & 3 deletions test/strict.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,31 @@
import * as t from '../src/index'
import { assertSuccess, assertFailure, assertStrictEqual } from './helpers'
import * as assert from 'assert'

describe('strict', () => {
it('should succeed validating a valid value', () => {
const T = t.strict(t.interface({ foo: t.string }))
const T = t.strict({ foo: t.string })
assertSuccess(t.validate({ foo: 'foo' }, T))
})

it('should return the same reference if validation succeeded', () => {
const T = t.strict(t.interface({ foo: t.string }))
const T = t.strict({ foo: t.string })
const value = { foo: 'foo' }
assertStrictEqual(t.validate(value, T), value)
})

it('should fail validating an invalid value', () => {
const T = t.strict(t.interface({ foo: t.string }))
const T = t.strict({ foo: t.string })
assertFailure(t.validate({ foo: 'foo', bar: 1, baz: true }, T), [
'Invalid value 1 supplied to : StrictType<{ foo: string }>/bar: never',
'Invalid value true supplied to : StrictType<{ foo: string }>/baz: never'
])
})

it('should assign a default name', () => {
const T1 = t.strict({ foo: t.string }, 'Foo')
assert.strictEqual(T1.name, 'Foo')
const T2 = t.strict({ foo: t.string })
assert.strictEqual(T2.name, 'StrictType<{ foo: string }>')
})
})
4 changes: 2 additions & 2 deletions typings-checker/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,10 +191,10 @@ const x31: TypeOf<typeof M1> = 1
// strict
//

const S1 = t.strict(t.interface({ name: t.string }))
const S1 = t.strict({ name: t.string })
type TS1 = TypeOf<typeof S1>
const x32: TS1 = { name: 'Giulio' }
const x33input = { name: 'foo', foo: 'foo' }
const x33: TS1 = x33input
// $ExpectError Argument of type 'StringType' is not assignable to parameter of type 'InterfaceType<any>'.
// $ExpectError Argument of type 'StringType' is not assignable to parameter of type 'Props'
const S2 = t.strict(t.string)

0 comments on commit f60d6d4

Please sign in to comment.