Skip to content

Commit e15a0aa

Browse files
committed
feat: move autoCast getters to separate functions
Every type used to have a `.autoCast` and `.autoCastAll` getter that returns a clone of the type with autocast parsers added. This PR changes that to be separate functions with the same name. This helps with future changes, because maintaining getters that return the `this` types is becoming increasingly hard. BREAKING CHANGE: The getters `.autoCast` and `.autoCastAll` are removed and moved to separately exported functions.
1 parent 4f9b0bd commit e15a0aa

24 files changed

+285
-234
lines changed

src/autocast.test.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,22 @@
1+
import { autoCast } from './autocast';
12
import { int, number, string } from './types';
23

34
test('autoCast should not override existing parsers', () => {
45
const HourOfDay = int.withConstraint('HourOfDay', n => 0 <= n && n < 24).withParser(number.andThen(n => n % 24));
56

67
const hour = HourOfDay(34);
7-
const afterAutoCast = HourOfDay.autoCast(34);
8+
const afterAutoCast = autoCast(HourOfDay)(34);
89

910
expect(hour).toBe(10);
1011
expect(afterAutoCast).toBe(10);
1112

1213
// Currently, types with parsers get no benefit from autoCast, because we don't know whether to apply the autoCast before or after the
1314
// custom parser.
1415
expect(() => HourOfDay('34')).toThrow('error in parser precondition of [HourOfDay]: expected a number, got a string ("34")');
15-
expect(() => HourOfDay.autoCast('34')).toThrow('error in parser precondition of [HourOfDay]: expected a number, got a string ("34")');
16+
expect(() => autoCast(HourOfDay)('34')).toThrow('error in parser precondition of [HourOfDay]: expected a number, got a string ("34")');
1617
});
1718

1819
test('autoCast instances should not leak', () => {
1920
const SmallString = string.withConstraint('SmallString', s => s.length < 10);
20-
expect(string.autoCast).not.toBe(SmallString.autoCast);
21+
expect(autoCast(string)).not.toBe(autoCast(SmallString));
2122
});

src/autocast.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { BaseTypeImpl, createType } from './base-type';
2+
import { autoCastFailure } from './symbols';
3+
import { bracketsIfNeeded, printValue } from './utils';
4+
5+
/**
6+
* Returns the same type, but with an auto-casting default parser installed.
7+
*
8+
* @remarks
9+
* Each type implementation provides its own auto-cast rules. See builtin types for examples of auto-cast rules.
10+
*/
11+
export function autoCast<T extends BaseTypeImpl<unknown>>(type: T): T {
12+
const autoCaster = type['autoCaster'];
13+
const typeParser = type['typeParser'];
14+
if (!autoCaster || typeParser) return type;
15+
return (type['_instanceCache'].autoCast ??= createType(type, {
16+
name: { configurable: true, value: `AutoCast<${bracketsIfNeeded(type.name, '&', '|')}>` },
17+
typeParser: { configurable: true, value: createAutoCastParser(autoCaster) },
18+
})) as T;
19+
}
20+
21+
/**
22+
* Create a recursive autocasting version of the given type.
23+
*
24+
* @remarks
25+
* This will replace any parser in the nested structure with the appropriate autocaster when applicable.
26+
*/
27+
export function autoCastAll<T extends BaseTypeImpl<unknown>>(type: T): T {
28+
return (type['_instanceCache'].autoCastAll ??= type['createAutoCastAllType']()) as T;
29+
}
30+
31+
function createAutoCastParser<ResultType, TypeConfig>(
32+
autoCaster: (this: BaseTypeImpl<ResultType, TypeConfig>, value: unknown) => unknown,
33+
): BaseTypeImpl<ResultType, TypeConfig>['typeParser'] {
34+
return function (this: BaseTypeImpl<ResultType, TypeConfig>, input) {
35+
const autoCastResult = autoCaster.call(this, input);
36+
return this.createResult(
37+
input,
38+
autoCastResult,
39+
autoCastResult !== autoCastFailure || {
40+
kind: 'custom message',
41+
message: `could not autocast value: ${printValue(input)}`,
42+
omitInput: true,
43+
},
44+
);
45+
};
46+
}

src/base-type.ts

Lines changed: 4 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { autoCast } from './autocast';
12
import type {
23
BasicType,
34
Branded,
@@ -23,18 +24,16 @@ import type {
2324
Validator,
2425
Visitor,
2526
} from './interfaces';
26-
import { autoCastFailure, designType } from './symbols';
27+
import { designType } from './symbols';
2728
import {
2829
addParserInputToResult,
2930
an,
3031
basicType,
31-
bracketsIfNeeded,
3232
castArray,
3333
decodeOptionalOptions,
3434
defaultStringify,
3535
isOneOrMore,
3636
prependContextToDetails,
37-
printValue,
3837
} from './utils';
3938
import { ValidationError } from './validation-error';
4039

@@ -122,34 +121,8 @@ export abstract class BaseTypeImpl<ResultType, TypeConfig = unknown> implements
122121
boundIs?: BaseTypeImpl<ResultType, TypeConfig>['is'];
123122
} = {};
124123

125-
/**
126-
* The same type, but with an auto-casting default parser installed.
127-
*
128-
* @remarks
129-
* Each type implementation provides its own auto-cast rules. See builtin types for examples of auto-cast rules.
130-
*/
131-
get autoCast(): this {
132-
// eslint-disable-next-line @typescript-eslint/unbound-method
133-
const { autoCaster, typeParser } = this;
134-
if (!autoCaster || typeParser) return this;
135-
return (this._instanceCache.autoCast ??= createType(this, {
136-
name: { configurable: true, value: `${bracketsIfNeeded(this.name)}.autoCast` },
137-
typeParser: { configurable: true, value: createAutoCastParser(autoCaster) },
138-
})) as this;
139-
}
140-
141-
/**
142-
* Create a recursive autocasting version of the current type.
143-
*
144-
* @remarks
145-
* This will replace any parser in the nested structure with the appropriate autocaster when applicable.
146-
*/
147-
get autoCastAll(): this {
148-
return (this._instanceCache.autoCastAll ??= this.createAutoCastAllType()) as this;
149-
}
150-
151-
protected createAutoCastAllType(): this {
152-
return this.autoCast;
124+
protected createAutoCastAllType(): Type<ResultType> {
125+
return autoCast(this as unknown as Type<ResultType>);
153126
}
154127

155128
/**
@@ -611,20 +584,3 @@ function getVisitedMap<ResultType>(me: BaseTypeImpl<ResultType, any>, options: V
611584
}
612585
return valueMap as Map<unknown, Result<ResultType>>;
613586
}
614-
615-
function createAutoCastParser<ResultType, TypeConfig>(
616-
autoCaster: (this: BaseTypeImpl<ResultType, TypeConfig>, value: unknown) => unknown,
617-
): BaseTypeImpl<ResultType, TypeConfig>['typeParser'] {
618-
return function (this: BaseTypeImpl<ResultType, TypeConfig>, input) {
619-
const autoCastResult = autoCaster.call(this, input);
620-
return this.createResult(
621-
input,
622-
autoCastResult,
623-
autoCastResult !== autoCastFailure || {
624-
kind: 'custom message',
625-
message: `could not autocast value: ${printValue(input)}`,
626-
omitInput: true,
627-
},
628-
);
629-
};
630-
}

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
export * from './autocast';
12
export * from './base-type';
23
export * from './error-reporter';
34
export * from './interfaces';

src/simple-type.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { autoCast } from './autocast';
12
import type { The } from './interfaces';
23
import { SimpleType } from './simple-type';
34
import { createExample } from './testutils';
@@ -37,7 +38,7 @@ describe(SimpleType, () => {
3738
expect(BufferType(buffer)).toBe(buffer);
3839
expect(() => BufferType([])).toThrow(`expected a [Buffer], got: []`);
3940
expect(() => BufferType(Uint8Array.of(1, 2, 3))).toThrow(`expected a [Buffer], got: "1,2,3"`);
40-
expect(BufferType.autoCast(Uint8Array.of(1, 2, 3))).toEqual(Buffer.of(1, 2, 3));
41+
expect(autoCast(BufferType)(Uint8Array.of(1, 2, 3))).toEqual(Buffer.of(1, 2, 3));
4142
});
4243

4344
test('has basic support for stringify', () => {

src/types.test.ts

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
/* eslint-disable @typescript-eslint/no-unsafe-return */
2+
import { autoCast } from './autocast';
23
import type { DeepUnbranded, MessageDetails, ObjectType, The, Type, Unbranded, Writable } from './interfaces';
34
import {
45
assignableTo,
@@ -103,24 +104,24 @@ testTypeImpl({
103104
});
104105

105106
testTypeImpl({
106-
name: 'Age.autoCast',
107-
type: Age.autoCast,
107+
name: 'AutoCast<Age>',
108+
type: autoCast(Age),
108109
basicType: 'number',
109110
validValues: [0, 1, Age.MAX],
110111
invalidValues: [
111-
[-1, 'error in [Age.autoCast]: the unborn miracle, got: -1'],
112-
[Age.MAX + 1, 'error in [Age.autoCast]: wow, that is really old, got: 200'],
113-
[-1.5, ['errors in [Age.autoCast]:', '', '- the unborn miracle, got: -1.5', '', '- expected a whole number, got: -1.5']],
114-
[1.5, 'error in [Age.autoCast]: expected a whole number, got: 1.5'],
115-
...defaultUsualSuspects(Age.autoCast),
112+
[-1, 'error in [AutoCast<Age>]: the unborn miracle, got: -1'],
113+
[Age.MAX + 1, 'error in [AutoCast<Age>]: wow, that is really old, got: 200'],
114+
[-1.5, ['errors in [AutoCast<Age>]:', '', '- the unborn miracle, got: -1.5', '', '- expected a whole number, got: -1.5']],
115+
[1.5, 'error in [AutoCast<Age>]: expected a whole number, got: 1.5'],
116+
...defaultUsualSuspects(autoCast(Age)),
116117
],
117118
validConversions: [
118119
[`${Age.MAX}`, Age.MAX],
119120
['0', 0],
120121
],
121122
invalidConversions: [
122-
['abc', 'error in parser of [Age.autoCast]: could not autocast value: "abc"'],
123-
['-1', 'error in [Age.autoCast]: the unborn miracle, got: -1, parsed from: "-1"'],
123+
['abc', 'error in parser of [AutoCast<Age>]: could not autocast value: "abc"'],
124+
['-1', 'error in [AutoCast<Age>]: the unborn miracle, got: -1, parsed from: "-1"'],
124125
],
125126
});
126127

@@ -589,7 +590,7 @@ testTypeImpl({
589590
});
590591

591592
const NestedParsers = object('NestedParsers', {
592-
outer: object({ inner: object({ value: number.autoCast }) }).withParser(inner => ({ inner })),
593+
outer: object({ inner: object({ value: autoCast(number) }) }).withParser(inner => ({ inner })),
593594
}).withParser(outer => ({ outer }));
594595

595596
testTypeImpl({
@@ -606,7 +607,7 @@ testTypeImpl({
606607
// and especially how they are related without bloating the error report.
607608
'(got: { outer: {} }, parsed from: {})',
608609
'',
609-
'- at <outer.inner>: missing property <value> [number.autoCast], got: {}',
610+
'- at <outer.inner>: missing property <value> [AutoCast<number>], got: {}',
610611
],
611612
],
612613
[
@@ -624,7 +625,7 @@ testTypeImpl({
624625
});
625626

626627
type ReplacedAutocastParser = The<typeof ReplacedAutocastParser>;
627-
const ReplacedAutocastParser = number.autoCast.withParser({ name: 'ReplacedAutocastParser', chain: false }, input => input);
628+
const ReplacedAutocastParser = autoCast(number).withParser({ name: 'ReplacedAutocastParser', chain: false }, input => input);
628629

629630
testTypeImpl({
630631
name: 'ReplacedAutocastParser',
@@ -635,7 +636,7 @@ testTypeImpl({
635636
});
636637

637638
type ChainedAutocastParser = The<typeof ChainedAutocastParser>;
638-
const ChainedAutocastParser = number.autoCast.withParser({ name: 'ChainedAutocastParser', chain: true }, input => input);
639+
const ChainedAutocastParser = autoCast(number).withParser({ name: 'ChainedAutocastParser', chain: true }, input => input);
639640

640641
testTypeImpl({
641642
name: 'ChainedAutocastParser',

src/types/array.test.ts

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { autoCast, autoCastAll } from '../autocast';
12
import type { The } from '../interfaces';
23
import { assignableTo, createExample, defaultUsualSuspects, testTypeImpl, testTypes } from '../testutils';
34
import { array } from './array';
@@ -32,18 +33,18 @@ testTypeImpl({
3233
});
3334

3435
testTypeImpl({
35-
name: 'string[].autoCast',
36-
type: array(string).autoCast,
36+
name: 'AutoCast<string[]>',
37+
type: autoCast(array(string)),
3738
basicType: 'array',
3839
// wrap arrays inside extra array because of the use of jest.each in testTypeImpl
3940
validValues: [[['a', 'b', 'bla']], [[]]],
4041
invalidValues: [
41-
['a', 'error in [string[].autoCast]: expected an array, got a string ("a")'],
42-
[{ 0: 'a', length: 1 }, 'error in [string[].autoCast]: expected an array, got an object ({ "0": "a", length: 1 })'],
42+
['a', 'error in [AutoCast<string[]>]: expected an array, got a string ("a")'],
43+
[{ 0: 'a', length: 1 }, 'error in [AutoCast<string[]>]: expected an array, got an object ({ "0": "a", length: 1 })'],
4344
[
4445
[1, 2, 3],
4546
[
46-
'errors in [string[].autoCast]:',
47+
'errors in [AutoCast<string[]>]:',
4748
'',
4849
'- at <[0]>: expected a string, got a number (1)',
4950
'',
@@ -58,43 +59,43 @@ testTypeImpl({
5859
['str', ['str']],
5960
],
6061
invalidConversions: [
61-
[1, ['errors in [string[].autoCast]:', '(got: [1], parsed from: 1)', '', '- at <[0]>: expected a string, got a number (1)']],
62+
[1, ['errors in [AutoCast<string[]>]:', '(got: [1], parsed from: 1)', '', '- at <[0]>: expected a string, got a number (1)']],
6263
],
6364
});
6465

6566
testTypeImpl({
66-
name: 'Array<number.autoCast>',
67-
type: array(number.autoCast),
67+
name: 'Array<AutoCast<number>>',
68+
type: array(autoCast(number)),
6869
basicType: 'array',
6970
// wrap arrays inside extra array because of the use of jest.each in testTypeImpl
7071
validValues: [[[1, 2, 3]], [[]]],
7172
invalidValues: [
72-
[1, 'error in [Array<number.autoCast>]: expected an array, got a number (1)'],
73-
[{ 0: 0 }, 'error in [Array<number.autoCast>]: expected an array, got an object ({ "0": 0 })'],
74-
[['1'], 'error in [Array<number.autoCast>] at <[0]>: expected a number, got a string ("1")'],
75-
...defaultUsualSuspects(array(number.autoCast)),
73+
[1, 'error in [Array<AutoCast<number>>]: expected an array, got a number (1)'],
74+
[{ 0: 0 }, 'error in [Array<AutoCast<number>>]: expected an array, got an object ({ "0": 0 })'],
75+
[['1'], 'error in [Array<AutoCast<number>>] at <[0]>: expected a number, got a string ("1")'],
76+
...defaultUsualSuspects(array(autoCast(number))),
7677
],
7778
validConversions: [
7879
[
7980
['1', '2', '3'],
8081
[1, 2, 3],
8182
],
8283
],
83-
invalidConversions: [[1, 'error in [Array<number.autoCast>]: expected an array, got a number (1)']],
84+
invalidConversions: [[1, 'error in [Array<AutoCast<number>>]: expected an array, got a number (1)']],
8485
});
8586

8687
testTypeImpl({
87-
name: 'Array<number.autoCast>.autoCast',
88-
type: array(number).autoCastAll,
88+
name: 'AutoCast<Array<AutoCast<number>>>',
89+
type: autoCastAll(array(number)),
8990
basicType: 'array',
9091
// wrap arrays inside extra array because of the use of jest.each in testTypeImpl
9192
validValues: [[[1, 2, 3]], [[]]],
9293
validConversions: [['1', [1]]],
9394
});
9495

9596
testTypeImpl({
96-
name: '(custom name).autoCast',
97-
type: array('custom name', number).autoCastAll,
97+
name: 'AutoCast<(custom name)>',
98+
type: autoCastAll(array('custom name', number)),
9899
});
99100

100101
type SmallArray = The<typeof SmallArray>;

src/types/array.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { autoCast, autoCastAll } from '../autocast';
12
import { BaseTypeImpl, createType } from '../base-type';
23
import type { ArrayTypeConfig, Result, TypeImpl, TypeOf, ValidationOptions, Visitor } from '../interfaces';
34
import {
@@ -90,7 +91,7 @@ define(ArrayType, 'basicType', 'array');
9091
// Defined outside class definition, because TypeScript somehow ends up in a wild-typings-goose-chase that takes
9192
// up to a minute or more. We have to make sure consuming libs don't have to pay this penalty ever.
9293
define(ArrayType, 'createAutoCastAllType', function (this: ArrayType<BaseTypeImpl<any>, any, any[]>) {
93-
return createType(new ArrayType(this.elementType.autoCastAll, this.typeConfig, this.isDefaultName ? undefined : this.name).autoCast);
94+
return createType(autoCast(new ArrayType(autoCastAll(this.elementType), this.typeConfig, this.isDefaultName ? undefined : this.name)));
9495
});
9596

9697
/**

src/types/boolean.test.ts

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { autoCast, autoCastAll } from '../autocast';
12
import { basicTypeMessage, testTypeImpl } from '../testutils';
23
import { boolean } from './boolean';
34

@@ -19,17 +20,17 @@ testTypeImpl({
1920
});
2021

2122
testTypeImpl({
22-
name: 'boolean.autoCast',
23-
type: boolean.autoCast,
23+
name: 'AutoCast<boolean>',
24+
type: autoCast(boolean),
2425
basicType: 'boolean',
2526
validValues: [true, false],
2627
invalidValues: [
27-
['false', basicTypeMessage(boolean.autoCast, 'false')],
28-
[NaN, basicTypeMessage(boolean.autoCast, NaN)],
29-
[null, basicTypeMessage(boolean.autoCast, null)],
30-
[undefined, basicTypeMessage(boolean.autoCast, undefined)],
31-
[0, basicTypeMessage(boolean.autoCast, 0)],
32-
[1, basicTypeMessage(boolean.autoCast, 1)],
28+
['false', basicTypeMessage(autoCast(boolean), 'false')],
29+
[NaN, basicTypeMessage(autoCast(boolean), NaN)],
30+
[null, basicTypeMessage(autoCast(boolean), null)],
31+
[undefined, basicTypeMessage(autoCast(boolean), undefined)],
32+
[0, basicTypeMessage(autoCast(boolean), 0)],
33+
[1, basicTypeMessage(autoCast(boolean), 1)],
3334
],
3435
validConversions: [
3536
['false', false],
@@ -40,12 +41,12 @@ testTypeImpl({
4041
[1, true],
4142
],
4243
invalidConversions: [
43-
[NaN, 'error in parser of [boolean.autoCast]: could not autocast value: NaN'],
44-
[null, 'error in parser of [boolean.autoCast]: could not autocast value: null'],
45-
[undefined, 'error in parser of [boolean.autoCast]: could not autocast value: undefined'],
44+
[NaN, 'error in parser of [AutoCast<boolean>]: could not autocast value: NaN'],
45+
[null, 'error in parser of [AutoCast<boolean>]: could not autocast value: null'],
46+
[undefined, 'error in parser of [AutoCast<boolean>]: could not autocast value: undefined'],
4647
],
4748
});
4849

4950
test('no autoCastAll', () => {
50-
expect(boolean.autoCastAll).toBe(boolean.autoCast);
51+
expect(autoCastAll(boolean)).toBe(autoCast(boolean));
5152
});

0 commit comments

Comments
 (0)