Skip to content

Commit fe15b70

Browse files
committed
make Type more "prismatic"
1 parent b7aaac1 commit fe15b70

File tree

2 files changed

+56
-40
lines changed

2 files changed

+56
-40
lines changed

src/index.ts

Lines changed: 54 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -16,24 +16,34 @@ export interface ValidationError {
1616
readonly value: any
1717
readonly context: Context
1818
}
19-
export type Is<A> = (value: any) => value is A
20-
export type Validation<A> = Either<Array<ValidationError>, A>
21-
export type Validate<A> = (value: any, context: Context) => Validation<A>
22-
export type Serialize<A> = (value: A) => any
23-
export type Any = Type<any>
2419

25-
type Errors = Array<ValidationError>
20+
export type Errors = Array<ValidationError>
21+
export type Validation<A> = Either<Errors, A>
22+
export type Is<A> = (value: any) => value is A
23+
export type Validate<S, A> = (value: S, context: Context) => Validation<A>
24+
export type Serialize<S, A> = (value: A) => S
25+
export type Any = Type<any, any>
2626

2727
export type TypeOf<RT extends Any> = RT['_A']
28+
export type InputOf<RT extends Any> = RT['_S']
2829

29-
export class Type<A> {
30+
export class Type<S, A> {
3031
readonly _A: A
32+
readonly _S: S
3133
constructor(
3234
readonly name: string,
3335
readonly is: Is<A>,
34-
readonly validate: Validate<A>,
35-
readonly serialize: Serialize<A>
36+
readonly validate: Validate<S, A>,
37+
readonly serialize: Serialize<S, A>
3638
) {}
39+
compose<B>(ab: Type<A, B>, name?: string): Type<S, B> {
40+
return new Type(
41+
name || `compose(${this.name}, ${ab.name})`,
42+
(v): v is B => this.is(v) && ab.is(v),
43+
(s, c) => this.validate(s, c).chain(a => ab.validate(a, c)),
44+
b => this.serialize(ab.serialize(b))
45+
)
46+
}
3747
}
3848

3949
export const getFunctionName = (f: any): string => f.displayName || f.name || `<function${f.length}>`
@@ -51,15 +61,16 @@ export const failure = <T>(value: any, context: Context): Validation<T> =>
5161

5262
export const success = <T>(value: T): Validation<T> => new Right<Errors, T>(value)
5363

54-
const getDefaultContext = <T>(type: Type<T>): Context => [{ key: '', type }]
64+
const getDefaultContext = (type: Any): Context => [{ key: '', type }]
5565

56-
export const validate = <T>(value: any, type: Type<T>): Validation<T> => type.validate(value, getDefaultContext(type))
66+
export const validate = <S, A>(value: S, type: Type<S, A>): Validation<A> =>
67+
type.validate(value, getDefaultContext(type))
5768

5869
//
5970
// basic types
6071
//
6172

62-
export class NullType extends Type<null> {
73+
export class NullType extends Type<any, null> {
6374
readonly _tag: 'NullType' = 'NullType'
6475
constructor() {
6576
super('null', (v): v is null => v === null, (v, c) => (this.is(v) ? success(v) : failure(v, c)), identity)
@@ -69,7 +80,7 @@ export class NullType extends Type<null> {
6980
/** An alias of `null` */
7081
export const nullType: NullType = new NullType()
7182

72-
export class UndefinedType extends Type<undefined> {
83+
export class UndefinedType extends Type<any, undefined> {
7384
readonly _tag: 'UndefinedType' = 'UndefinedType'
7485
constructor() {
7586
super(
@@ -83,7 +94,7 @@ export class UndefinedType extends Type<undefined> {
8394

8495
const undefinedType: UndefinedType = new UndefinedType()
8596

86-
export class AnyType extends Type<any> {
97+
export class AnyType extends Type<any, any> {
8798
readonly _tag: 'AnyType' = 'AnyType'
8899
constructor() {
89100
super('any', (_): _ is any => true, success, identity)
@@ -92,7 +103,7 @@ export class AnyType extends Type<any> {
92103

93104
export const any: AnyType = new AnyType()
94105

95-
export class NeverType extends Type<never> {
106+
export class NeverType extends Type<any, never> {
96107
readonly _tag: 'NeverType' = 'NeverType'
97108
constructor() {
98109
super(
@@ -108,7 +119,7 @@ export class NeverType extends Type<never> {
108119

109120
export const never: NeverType = new NeverType()
110121

111-
export class StringType extends Type<string> {
122+
export class StringType extends Type<any, string> {
112123
readonly _tag: 'StringType' = 'StringType'
113124
constructor() {
114125
super(
@@ -122,7 +133,7 @@ export class StringType extends Type<string> {
122133

123134
export const string: StringType = new StringType()
124135

125-
export class NumberType extends Type<number> {
136+
export class NumberType extends Type<any, number> {
126137
readonly _tag: 'NumberType' = 'NumberType'
127138
constructor() {
128139
super(
@@ -136,7 +147,7 @@ export class NumberType extends Type<number> {
136147

137148
export const number: NumberType = new NumberType()
138149

139-
export class BooleanType extends Type<boolean> {
150+
export class BooleanType extends Type<any, boolean> {
140151
readonly _tag: 'BooleanType' = 'BooleanType'
141152
constructor() {
142153
super(
@@ -150,7 +161,7 @@ export class BooleanType extends Type<boolean> {
150161

151162
export const boolean: BooleanType = new BooleanType()
152163

153-
export class AnyArrayType extends Type<Array<any>> {
164+
export class AnyArrayType extends Type<any, Array<any>> {
154165
readonly _tag: 'AnyArrayType' = 'AnyArrayType'
155166
constructor() {
156167
super(
@@ -164,7 +175,7 @@ export class AnyArrayType extends Type<Array<any>> {
164175

165176
const arrayType: AnyArrayType = new AnyArrayType()
166177

167-
export class AnyDictionaryType extends Type<{ [key: string]: any }> {
178+
export class AnyDictionaryType extends Type<any, { [key: string]: any }> {
168179
readonly _tag: 'AnyDictionaryType' = 'AnyDictionaryType'
169180
constructor() {
170181
super(
@@ -178,7 +189,7 @@ export class AnyDictionaryType extends Type<{ [key: string]: any }> {
178189

179190
export const Dictionary: AnyDictionaryType = new AnyDictionaryType()
180191

181-
export class ObjectType extends Type<object> {
192+
export class ObjectType extends Type<any, object> {
182193
readonly _tag: 'ObjectType' = 'ObjectType'
183194
constructor() {
184195
super('object', Dictionary.is, Dictionary.validate, identity)
@@ -187,7 +198,7 @@ export class ObjectType extends Type<object> {
187198

188199
export const object: ObjectType = new ObjectType()
189200

190-
export class FunctionType extends Type<Function> {
201+
export class FunctionType extends Type<any, Function> {
191202
readonly _tag: 'FunctionType' = 'FunctionType'
192203
constructor() {
193204
super(
@@ -205,7 +216,7 @@ const functionType: FunctionType = new FunctionType()
205216
// refinements
206217
//
207218

208-
export class RefinementType<RT extends Any> extends Type<TypeOf<RT>> {
219+
export class RefinementType<RT extends Any> extends Type<InputOf<RT>, TypeOf<RT>> {
209220
readonly _tag: 'RefinementType' = 'RefinementType'
210221
constructor(
211222
readonly type: RT,
@@ -233,7 +244,7 @@ export const Integer = refinement(number, n => n % 1 === 0, 'Integer')
233244
// literal types
234245
//
235246

236-
export class LiteralType<V extends string | number | boolean> extends Type<V> {
247+
export class LiteralType<V extends string | number | boolean> extends Type<any, V> {
237248
readonly _tag: 'LiteralType' = 'LiteralType'
238249
constructor(readonly value: V, readonly name: string = JSON.stringify(value)) {
239250
super(name, (v): v is V => v === value, (v, c) => (this.is(v) ? success(value) : failure(v, c)), identity)
@@ -247,7 +258,7 @@ export const literal = <V extends string | number | boolean>(value: V, name?: st
247258
// keyof types
248259
//
249260

250-
export class KeyofType<D extends { [key: string]: any }> extends Type<keyof D> {
261+
export class KeyofType<D extends { [key: string]: any }> extends Type<any, keyof D> {
251262
readonly _tag: 'KeyofType' = 'KeyofType'
252263
constructor(readonly keys: D, readonly name: string = `(keyof ${JSON.stringify(Object.keys(keys))})`) {
253264
super(
@@ -266,10 +277,15 @@ export const keyof = <D extends { [key: string]: any }>(keys: D, name?: string):
266277
// recursive types
267278
//
268279

269-
export class RecursiveType<T> extends Type<T> {
280+
export class RecursiveType<T> extends Type<any, T> {
270281
readonly _tag: 'RecursiveType' = 'RecursiveType'
271282
readonly type: Any
272-
constructor(readonly name: string, is: Is<T>, readonly validate: Validate<T>, readonly serialize: Serialize<T>) {
283+
constructor(
284+
readonly name: string,
285+
is: Is<T>,
286+
readonly validate: Validate<any, T>,
287+
readonly serialize: Serialize<any, T>
288+
) {
273289
super(name, is, validate, serialize)
274290
}
275291
}
@@ -286,7 +302,7 @@ export const recursion = <T>(name: string, definition: (self: Any) => Any): Recu
286302
// arrays
287303
//
288304

289-
export class ArrayType<RT extends Any> extends Type<Array<TypeOf<RT>>> {
305+
export class ArrayType<RT extends Any> extends Type<any, Array<TypeOf<RT>>> {
290306
readonly _tag: 'ArrayType' = 'ArrayType'
291307
constructor(readonly type: RT, readonly name: string = `Array<${type.name}>`) {
292308
super(
@@ -338,7 +354,7 @@ const useIdentity = (props: Props): boolean => {
338354
return true
339355
}
340356

341-
export class InterfaceType<P extends Props> extends Type<InterfaceOf<P>> {
357+
export class InterfaceType<P extends Props> extends Type<any, InterfaceOf<P>> {
342358
readonly _tag: 'InterfaceType' = 'InterfaceType'
343359
constructor(readonly props: P, readonly name: string = getNameFromProps(props)) {
344360
super(
@@ -396,9 +412,9 @@ export const type = <P extends Props>(props: P, name?: string): InterfaceType<P>
396412
// TODO remove this once https://github.com/Microsoft/TypeScript/issues/14041 is fixed
397413
export type PartialOf<P extends Props> = { [K in keyof P]?: TypeOf<P[K]> }
398414
// TODO remove this once https://github.com/Microsoft/TypeScript/issues/14041 is fixed
399-
export type PartialPropsOf<P extends Props> = { [K in keyof P]: UnionType<[P[K], Type<undefined>]> }
415+
export type PartialPropsOf<P extends Props> = { [K in keyof P]: UnionType<[P[K], UndefinedType]> }
400416

401-
export class PartialType<P extends Props> extends Type<PartialOf<P>> {
417+
export class PartialType<P extends Props> extends Type<any, PartialOf<P>> {
402418
readonly _tag: 'PartialType' = 'PartialType'
403419
constructor(readonly props: P, name?: string) {
404420
super(
@@ -432,7 +448,7 @@ export const partial = <P extends Props>(props: P, name?: string): PartialType<P
432448
// dictionaries
433449
//
434450

435-
export class DictionaryType<C extends Any> extends Type<{ [key: string]: TypeOf<C> }> {
451+
export class DictionaryType<C extends Any> extends Type<any, { [key: string]: TypeOf<C> }> {
436452
readonly _tag: 'DictionaryType' = 'DictionaryType'
437453
constructor(readonly type: C, readonly name: string = `{ [key: string]: ${type.name} }`) {
438454
super(
@@ -476,7 +492,7 @@ export const dictionary = <C extends Any>(codomain: C, name?: string): Dictionar
476492
// unions
477493
//
478494

479-
export class UnionType<RTS extends [Any]> extends Type<TypeOf<RTS['_A']>> {
495+
export class UnionType<RTS extends [Any]> extends Type<any, TypeOf<RTS['_A']>> {
480496
readonly _tag: 'UnionType' = 'UnionType'
481497
constructor(readonly types: RTS, readonly name: string = `(${types.map(type => type.name).join(' | ')})`) {
482498
super(
@@ -512,7 +528,7 @@ export const union = <RTS extends [Any]>(types: RTS, name?: string): UnionType<R
512528
// intersections
513529
//
514530

515-
export class IntersectionType<RTS extends Array<Any>, I> extends Type<I> {
531+
export class IntersectionType<RTS extends Array<Any>, I> extends Type<any, I> {
516532
readonly _tag: 'IntersectionType' = 'IntersectionType'
517533
constructor(readonly types: RTS, readonly name: string = `(${types.map(type => type.name).join(' & ')})`) {
518534
super(
@@ -574,7 +590,7 @@ export function intersection<RTS extends Array<Any>>(types: RTS, name?: string):
574590
// tuples
575591
//
576592

577-
export class TupleType<RTS extends Array<Any>, I> extends Type<I> {
593+
export class TupleType<RTS extends Array<Any>, I> extends Type<any, I> {
578594
readonly _tag: 'TupleType' = 'TupleType'
579595
constructor(readonly types: RTS, readonly name: string = `[${types.map(type => type.name).join(', ')}]`) {
580596
super(
@@ -631,7 +647,7 @@ export function tuple<RTS extends Array<Any>>(types: RTS, name?: string): TupleT
631647
// readonly
632648
//
633649

634-
export class ReadonlyType<RT extends Any> extends Type<Readonly<TypeOf<RT>>> {
650+
export class ReadonlyType<RT extends Any> extends Type<any, Readonly<TypeOf<RT>>> {
635651
readonly _tag: 'ReadonlyType' = 'ReadonlyType'
636652
constructor(readonly type: RT, readonly name: string = `Readonly<${type.name}>`) {
637653
super(
@@ -655,7 +671,7 @@ export const readonly = <RT extends Any>(type: RT, name?: string): ReadonlyType<
655671
// readonlyArray
656672
//
657673

658-
export class ReadonlyArrayType<RT extends Any> extends Type<ReadonlyArray<TypeOf<RT>>> {
674+
export class ReadonlyArrayType<RT extends Any> extends Type<any, ReadonlyArray<TypeOf<RT>>> {
659675
readonly _tag: 'ReadonlyArrayType' = 'ReadonlyArrayType'
660676
constructor(readonly type: RT, readonly name: string = `ReadonlyArray<${type.name}>`) {
661677
super(
@@ -681,7 +697,7 @@ export class ReadonlyArrayType<RT extends Any> extends Type<ReadonlyArray<TypeOf
681697
export const readonlyArray = <RT extends Any>(type: RT, name?: string): ReadonlyArrayType<RT> =>
682698
new ReadonlyArrayType(type, name)
683699

684-
export class StrictType<P extends Props> extends Type<InterfaceOf<P>> {
700+
export class StrictType<P extends Props> extends Type<any, InterfaceOf<P>> {
685701
readonly _tag: 'StrictType' = 'StrictType'
686702
constructor(readonly props: P, name?: string) {
687703
super(

test/helpers.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,14 @@ export function assertDeepEqual<T>(validation: t.Validation<T>, value: any): voi
2020
assert.deepEqual(validation.fold<any>(t.identity, t.identity), value)
2121
}
2222

23-
export const number2 = new t.Type<number>(
23+
export const number2 = new t.Type<any, number>(
2424
'number2',
2525
t.number.is,
2626
(v, c) => t.number.validate(v, c).map(n => n * 2),
2727
t.identity
2828
)
2929

30-
export const DateFromNumber = new t.Type<Date>(
30+
export const DateFromNumber = new t.Type<any, Date>(
3131
'DateFromNumber',
3232
(v): v is Date => v instanceof Date,
3333
(v, c) =>

0 commit comments

Comments
 (0)