Skip to content

Commit c46600d

Browse files
committed
feat: add instanceOf
1 parent 3247ae1 commit c46600d

File tree

5 files changed

+93
-2
lines changed

5 files changed

+93
-2
lines changed

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ The validation errors are detailed. Adapted from the brilliant work in `flow-run
3434
- [`t.simpleObject({ foo: t.string() })`](#tsimpleobject-foo-tstring-)
3535
- [`t.object`](#tobject)
3636
- [`t.record(t.string(), t.number())`](#trecordtstring-tnumber)
37+
- [`t.instanceof(Date)`](#tinstanceofdate)
3738
- [`t.tuple(t.string(), t.number())`](#ttupletstring-tnumber)
3839
- [`t.allOf(A, B)`](#tallofa-b)
3940
- [`t.oneOf(t.string(), t.number())`](#toneoftstring-tnumber)
@@ -259,6 +260,10 @@ You can also use the `t.optionalNullOr(t.string())` as a shorthand for
259260
260261
A validator that requires the value to be `Record<string, number>`.
261262
263+
### `t.instanceOf(Date)`
264+
265+
A validator that requires the value to be an instance of `Date`.
266+
262267
### `t.tuple(t.string(), t.number())`
263268
264269
A validator that requires the value to be `[string, number]`.

src/errorReporting/typeOf.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@ import { keyToString } from './keyToString'
33
export default function typeOf(value: any): string {
44
if (value == null) return String(value)
55
if (typeof value !== 'object') return typeof value
6-
if (value.constructor && value.constructor !== Object)
7-
return value.constructor.name
6+
const constructor = value.prototype
7+
? value.prototype.constructor
8+
: value.constructor
9+
if (constructor && constructor !== Object) return constructor.name
810
return `{\n${Object.keys(value)
911
.map(key => ` ${keyToString(key)}: ${typeOf(value[key])}`)
1012
.join(',\n')}\n}`

src/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import AnyType from './types/AnyType'
33
import ArrayType from './types/ArrayType'
44
import BooleanLiteralType from './types/BooleanLiteralType'
55
import BooleanType from './types/BooleanType'
6+
import InstanceOfType from './types/InstanceOfType'
67
import IntersectionType from './types/IntersectionType'
78
import NullLiteralType from './types/NullLiteralType'
89
import UndefinedLiteralType from './types/UndefinedLiteralType'
@@ -28,6 +29,7 @@ export {
2829
ArrayType,
2930
BooleanLiteralType,
3031
BooleanType,
32+
InstanceOfType,
3133
IntersectionType,
3234
NullLiteralType,
3335
UndefinedLiteralType,
@@ -171,6 +173,10 @@ export const record = <K extends string | number | symbol, V>(
171173
value: Type<V>
172174
): RecordType<K, V> => new RecordType(key, value)
173175

176+
export const instanceOf = <T extends { new (...args: any[]): any }>(
177+
classType: T
178+
): Type<T> => new InstanceOfType(classType)
179+
174180
export const tuple = <T extends Type<any>[]>(
175181
...types: T
176182
): Type<{ [Index in keyof T]: T[Index] extends Type<infer E> ? E : never }> =>

src/instanceOf.spec.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import * as t from './'
2+
import { expect } from 'chai'
3+
import dedent from 'dedent-js'
4+
5+
class Foo {}
6+
7+
describe(`t.instanceOf`, function() {
8+
it(`accepts instances of class type`, function() {
9+
t.instanceOf(Date).assert(new Date())
10+
t.instanceOf(Foo).assert(new Foo())
11+
expect(t.instanceOf(Date).accepts(new Date())).to.be.true
12+
expect(t.instanceOf(Foo).accepts(new Foo())).to.be.true
13+
})
14+
it(`rejects not instances of class type`, function() {
15+
expect(() => t.instanceOf(Date).assert({})).to.throw(
16+
t.RuntimeTypeError,
17+
dedent`
18+
Value must be an instance of Date
19+
20+
Expected: Date
21+
22+
Actual Value: {}
23+
24+
Actual Type: {
25+
26+
}`
27+
)
28+
expect(t.instanceOf(Date).accepts(new Foo())).to.be.false
29+
expect(() => t.instanceOf(Date).assert(new Foo())).to.throw(
30+
t.RuntimeTypeError,
31+
dedent`
32+
Value must be an instance of Date
33+
34+
Expected: Date
35+
36+
Actual: Foo`
37+
)
38+
expect(t.instanceOf(Date).accepts(new Foo())).to.be.false
39+
})
40+
})

src/types/InstanceOfType.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import Type from './Type'
2+
3+
import getErrorMessage from '../getErrorMessage'
4+
import Validation, { ErrorTuple, IdentifierPath } from '../Validation'
5+
6+
export default class InstanceOfType<
7+
T extends { new (...args: any[]): any }
8+
> extends Type<T> {
9+
typeName = 'InstanceOfType'
10+
classType: T
11+
12+
constructor(classType: T) {
13+
super()
14+
this.classType = classType
15+
}
16+
17+
*errors(
18+
validation: Validation<any>,
19+
path: IdentifierPath,
20+
input: any
21+
): Generator<ErrorTuple, void, void> {
22+
if (!(input instanceof this.classType)) {
23+
yield [
24+
path,
25+
getErrorMessage('ERR_EXPECT_INSTANCEOF', this.toString()),
26+
this,
27+
]
28+
}
29+
}
30+
31+
accepts(input: any): boolean {
32+
return input instanceof this.classType
33+
}
34+
35+
toString(): string {
36+
return this.classType.prototype.constructor.name
37+
}
38+
}

0 commit comments

Comments
 (0)