Skip to content

Commit efa6fe2

Browse files
committed
fix: records, test intersections and unions
1 parent 26fc46c commit efa6fe2

11 files changed

+298
-196
lines changed

src/index.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ import UndefinedLiteralType from './types/UndefinedLiteralType'
99
import NumberType from './types/NumberType'
1010
import NumericLiteralType from './types/NumericLiteralType'
1111
import ObjectType from './types/ObjectType'
12-
import ObjectTypeIndexer from './types/ObjectTypeIndexer'
1312
import ObjectTypeProperty from './types/ObjectTypeProperty'
13+
import RecordType from './types/RecordType'
1414
import StringLiteralType from './types/StringLiteralType'
1515
import StringType from './types/StringType'
1616
import SymbolLiteralType from './types/SymbolLiteralType'
@@ -34,8 +34,8 @@ export {
3434
NumberType,
3535
NumericLiteralType,
3636
ObjectType,
37-
ObjectTypeIndexer,
3837
ObjectTypeProperty,
38+
RecordType,
3939
StringLiteralType,
4040
StringType,
4141
SymbolLiteralType,
@@ -149,27 +149,27 @@ export const object = <S extends Record<string | number | symbol, unknown>>({
149149
Boolean(getOptional(type))
150150
)
151151
),
152-
[],
153152
exact
154153
) as any
155154

156155
type Properties = Record<string | number | symbol, Type<any>>
157156

158157
export function simpleObject<Required extends Properties>(
159-
required: Required
158+
required: Required,
159+
{ exact }: { exact?: boolean } = {}
160160
): ObjectType<{ [K in keyof Required]: Required[K]['__type'] }> {
161161
return new ObjectType(
162162
[...Object.entries(required || [])].map(
163163
([key, type]) => new ObjectTypeProperty(key, type as Type<any>, false)
164-
)
164+
),
165+
exact
165166
) as any
166167
}
167168

168169
export const record = <K extends string | number | symbol, V>(
169170
key: Type<K>,
170171
value: Type<V>
171-
): ObjectType<Record<K, V>> =>
172-
new ObjectType([], [new ObjectTypeIndexer('key', key, value)]) as any
172+
): RecordType<K, V> => new RecordType(key, value)
173173

174174
export const tuple = <T extends []>(
175175
...types: { [Index in keyof T]: Type<T[Index]> }

src/intersection.spec.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import * as t from './'
2+
import { expect } from 'chai'
3+
import dedent from 'dedent-js'
4+
import typeOf from './errorReporting/typeOf'
5+
6+
describe(`t.intersection`, function() {
7+
const ObjectIntersection = t.intersection(
8+
t.simpleObject({ foo: t.number() }, { exact: false }),
9+
t.simpleObject({ bar: t.string() }, { exact: false })
10+
)
11+
it(`accepts valid values`, function() {
12+
for (const value of [
13+
{ foo: 2, bar: 'hello' },
14+
{ foo: -5, bar: 'world' },
15+
]) {
16+
ObjectIntersection.assert(value)
17+
expect(ObjectIntersection.accepts(value)).to.be.true
18+
}
19+
})
20+
it(`rejects invalid values`, function() {
21+
expect(() => ObjectIntersection.assert({ foo: 3 })).to.throw(
22+
t.RuntimeTypeError,
23+
dedent`
24+
Value must have property: bar
25+
26+
Expected: {
27+
bar: string
28+
}
29+
30+
Actual Value: {
31+
"foo": 3
32+
}
33+
34+
Actual Type: {
35+
foo: number
36+
}`
37+
)
38+
expect(ObjectIntersection.accepts({ foo: 3 })).to.be.false
39+
expect(() => ObjectIntersection.assert({ bar: 'hello' })).to.throw(
40+
t.RuntimeTypeError,
41+
dedent`
42+
Value must have property: foo
43+
44+
Expected: {
45+
foo: number
46+
}
47+
48+
Actual Value: {
49+
"bar": "hello"
50+
}
51+
52+
Actual Type: {
53+
bar: string
54+
}`
55+
)
56+
expect(ObjectIntersection.accepts({ bar: 'hello' })).to.be.false
57+
})
58+
})

src/object.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ describe(`t.object`, function() {
4848
)
4949
})
5050
it(`rejects everything else`, function() {
51-
for (const value of [true, 'foo', undefined, 2, []]) {
51+
for (const value of [true, 'foo', null, undefined, 2, []]) {
5252
expect(Person.accepts(value)).to.be.false
5353
expect(() => Person.assert(value)).to.throw(
5454
t.RuntimeTypeError,

src/record.spec.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import * as t from './'
2+
import { expect } from 'chai'
3+
import dedent from 'dedent-js'
4+
import typeOf from './errorReporting/typeOf'
5+
6+
describe(`t.record`, function() {
7+
const Numbers = t.record(t.string(), t.number())
8+
it(`accepts matching records`, function() {
9+
for (const value of [{ a: 1 }, { a: 1, b: 2 }]) {
10+
Numbers.assert(value)
11+
expect(Numbers.accepts(value)).to.be.true
12+
}
13+
})
14+
it(`rejects values that don't match`, function() {
15+
const value = { a: 'one' }
16+
expect(() => Numbers.assert(value, '', ['value'])).to.throw(
17+
t.RuntimeTypeError,
18+
dedent`
19+
value.a must be a number
20+
21+
Expected: number
22+
23+
Actual Value: "one"
24+
25+
Actual Type: string`
26+
)
27+
})
28+
it(`rejects everything else`, function() {
29+
for (const value of [true, 'foo', null, undefined, 2, []]) {
30+
expect(Numbers.accepts(value)).to.be.false
31+
expect(() => Numbers.assert(value)).to.throw(
32+
t.RuntimeTypeError,
33+
dedent`
34+
Value must be an object
35+
36+
Expected: Record<string, number>
37+
38+
Actual Value: ${JSON.stringify(value)}
39+
40+
Actual Type: ${typeOf(value)}`
41+
)
42+
}
43+
})
44+
})

src/ref.spec.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,9 @@ describe(`t.ref`, function() {
6161
node.left.right should not contain the key: bar
6262
6363
Expected: {
64-
value: any;
65-
left?: Node;
66-
right?: Node;
64+
value: any
65+
left?: Node
66+
right?: Node
6767
}
6868
6969
Actual Value: {

src/types/IntersectionType.ts

Lines changed: 0 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
11
import Type from './Type'
2-
3-
import { Property } from './ObjectType'
42
import Validation, { ErrorTuple, IdentifierPath } from '../Validation'
53

64
export default class IntersectionType<T> extends Type<T> {
@@ -24,40 +22,6 @@ export default class IntersectionType<T> extends Type<T> {
2422
}
2523
}
2624

27-
/**
28-
* Get a property with the given name, or undefined if it does not exist.
29-
*/
30-
getProperty<K extends string | number | symbol>(
31-
key: K
32-
): Property<K, any> | null | undefined {
33-
const { types } = this
34-
const { length } = types
35-
for (let i = length - 1; i >= 0; i--) {
36-
const type: any = types[i]
37-
if (typeof type.getProperty === 'function') {
38-
const prop = type.getProperty(key)
39-
if (prop) {
40-
return prop
41-
}
42-
}
43-
}
44-
}
45-
46-
/**
47-
* Determine whether a property with the given name exists.
48-
*/
49-
hasProperty(key: string): boolean {
50-
const { types } = this
51-
const { length } = types
52-
for (let i = 0; i < length; i++) {
53-
const type: any = types[i]
54-
if (typeof type.hasProperty === 'function' && type.hasProperty(key)) {
55-
return true
56-
}
57-
}
58-
return false
59-
}
60-
6125
accepts(input: any): boolean {
6226
const { types } = this
6327
const { length } = types

src/types/ObjectType.ts

Lines changed: 3 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,6 @@
11
import Type from './Type'
22

33
import ObjectTypeProperty from './ObjectTypeProperty'
4-
import ObjectTypeIndexer from './ObjectTypeIndexer'
5-
6-
export type Property<K extends string | number | symbol, V> =
7-
| ObjectTypeProperty<K, V>
8-
| ObjectTypeIndexer<K, V>
94

105
import getErrorMessage from '../getErrorMessage'
116
import Validation, { ErrorTuple, IdentifierPath } from '../Validation'
@@ -23,17 +18,14 @@ import { keyToString } from '../errorReporting/typeOf'
2318
export default class ObjectType<T extends {}> extends Type<T> {
2419
typeName = 'ObjectType'
2520
readonly properties: ObjectTypeProperty<keyof T, any>[]
26-
readonly indexers: ObjectTypeIndexer<any, any>[]
2721
readonly exact: boolean
2822

2923
constructor(
3024
properties: ObjectTypeProperty<keyof T, any>[] = [],
31-
indexers: ObjectTypeIndexer<any, any>[] = [],
3225
exact = true
3326
) {
3427
super()
3528
this.properties = properties
36-
this.indexers = indexers
3729
this.exact = exact
3830
properties.forEach(prop => (prop.__objectType = this))
3931
}
@@ -58,15 +50,7 @@ export default class ObjectType<T extends {}> extends Type<T> {
5850
}
5951
validation.startCycle(this, input)
6052

61-
if (this.indexers.length > 0) {
62-
if (input instanceof Object && Array.isArray(input)) {
63-
yield [path, getErrorMessage('ERR_EXPECT_OBJECT'), this]
64-
return
65-
}
66-
yield* collectErrorsWithIndexers(this, validation, path, input)
67-
} else {
68-
yield* collectErrorsWithoutIndexers(this, validation, path, input)
69-
}
53+
yield* collectErrorsWithoutIndexers(this, validation, path, input)
7054
if (this.exact) {
7155
yield* collectErrorsExact(this, validation, path, input)
7256
}
@@ -86,11 +70,7 @@ export default class ObjectType<T extends {}> extends Type<T> {
8670
startValidationCycle(this, input)
8771

8872
let result
89-
if (this.indexers.length > 0) {
90-
result = acceptsWithIndexers(this, input)
91-
} else {
92-
result = acceptsWithoutIndexers(this, input)
93-
}
73+
result = acceptsWithoutIndexers(this, input)
9474
if (result && this.exact) {
9575
result = acceptsExact(this, input)
9676
}
@@ -99,7 +79,7 @@ export default class ObjectType<T extends {}> extends Type<T> {
9979
}
10080

10181
toString(): string {
102-
const { properties, indexers } = this
82+
const { properties } = this
10383
if (inToStringCycle(this)) {
10484
return '$Cycle<Record<string, any>>'
10585
}
@@ -108,45 +88,11 @@ export default class ObjectType<T extends {}> extends Type<T> {
10888
for (let i = 0; i < properties.length; i++) {
10989
body.push(properties[i].toString())
11090
}
111-
for (let i = 0; i < indexers.length; i++) {
112-
body.push(indexers[i].toString())
113-
}
11491
endToStringCycle(this)
11592
return `{\n${indent(body.join('\n'))}\n}`
11693
}
11794
}
11895

119-
function acceptsWithIndexers(
120-
type: ObjectType<any>,
121-
input: Record<string, any>
122-
): boolean {
123-
const { properties, indexers } = type
124-
const seen = []
125-
for (let i = 0; i < properties.length; i++) {
126-
const property = properties[i]
127-
if (!property.accepts(input)) {
128-
return false
129-
}
130-
seen.push(property.key)
131-
}
132-
loop: for (const key in input) {
133-
if (seen.indexOf(key) !== -1) {
134-
continue
135-
}
136-
const value = (input as any)[key]
137-
for (let i = 0; i < indexers.length; i++) {
138-
const indexer = indexers[i]
139-
if (indexer.acceptsKey(key) && indexer.acceptsValue(value)) {
140-
continue loop
141-
}
142-
}
143-
144-
// if we got this far the key / value did not accepts any indexers.
145-
return false
146-
}
147-
return true
148-
}
149-
15096
function acceptsWithoutIndexers(
15197
type: ObjectType<any>,
15298
input: Record<string, any>
@@ -175,36 +121,6 @@ function acceptsExact(
175121
return true
176122
}
177123

178-
function* collectErrorsWithIndexers(
179-
type: ObjectType<any>,
180-
validation: Validation<any>,
181-
path: IdentifierPath,
182-
input: Record<string, any>
183-
): Generator<ErrorTuple, void, void> {
184-
const { properties, indexers } = type
185-
const seen = []
186-
for (let i = 0; i < properties.length; i++) {
187-
const property = properties[i]
188-
yield* property.errors(validation, path, input)
189-
seen.push(property.key)
190-
}
191-
loop: for (const key in input) {
192-
if (seen.indexOf(key) !== -1) {
193-
continue
194-
}
195-
const value = (input as any)[key]
196-
for (let i = 0; i < indexers.length; i++) {
197-
const indexer = indexers[i]
198-
if (indexer.acceptsKey(key) && indexer.acceptsValue(value)) {
199-
continue loop
200-
}
201-
}
202-
203-
// if we got this far the key / value was not accepted by any indexers.
204-
yield [path.concat(key), getErrorMessage('ERR_NO_INDEXER'), type]
205-
}
206-
}
207-
208124
function* collectErrorsWithoutIndexers(
209125
type: ObjectType<any>,
210126
validation: Validation<any>,

0 commit comments

Comments
 (0)