diff --git a/.vscode/settings.json b/.vscode/settings.json index eb81e21..e0555ab 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -14,5 +14,6 @@ "etc/**/*": true, "markdown/**/*": true, "node_modules/**/*": true - } + }, + "task.allowAutomaticTasks": "on" } diff --git a/etc/types.api.md b/etc/types.api.md index b4d0538..d0c633e 100644 --- a/etc/types.api.md +++ b/etc/types.api.md @@ -62,7 +62,7 @@ export abstract class BaseTypeImpl implements get autoCastAll(): this; protected autoCaster?(this: BaseTypeImpl, value: unknown): unknown; abstract readonly basicType: BasicType | 'mixed'; - check(input: unknown): ResultType; + get check(): (this: void, input: unknown) => ResultType; protected combineConfig(oldConfig: TypeConfig, newConfig: TypeConfig): TypeConfig; construct(input: unknown): ResultType; // (undocumented) @@ -70,7 +70,7 @@ export abstract class BaseTypeImpl implements protected createResult(input: unknown, result: unknown, validatorResult: ValidationResult): Result; readonly enumerableLiteralDomain?: Iterable; extendWith(factory: (type: this) => E): this & E; - is(input: unknown): input is ResultType; + get is(): (this: void, input: Input) => input is unknown extends Input ? ResultType & Input : Input extends ResultType ? Input : never; literal(input: DeepUnbranded): ResultType; abstract readonly name: string; or(_other: BaseTypeImpl): Type; diff --git a/jest.config.js b/jest.config.js index f83b194..6a266f2 100644 --- a/jest.config.js +++ b/jest.config.js @@ -12,10 +12,10 @@ const config = { restoreMocks: true, coverageThreshold: { global: { - statements: 99, - branches: 96, - functions: 99, - lines: 99, + statements: -4, + branches: -21, + functions: -2, + lines: 100, }, }, moduleNameMapper: { '^(.*)\\.js$': ['$1.js', '$1.ts'] }, diff --git a/markdown/types.basetypeimpl.check.md b/markdown/types.basetypeimpl.check.md index 1715571..cdd7ac3 100644 --- a/markdown/types.basetypeimpl.check.md +++ b/markdown/types.basetypeimpl.check.md @@ -2,26 +2,16 @@ [Home](./index.md) > [@skunkteam/types](./types.md) > [BaseTypeImpl](./types.basetypeimpl.md) > [check](./types.basetypeimpl.check.md) -## BaseTypeImpl.check() method +## BaseTypeImpl.check property Asserts that a value conforms to this Type and returns the input as is, if it does. **Signature:** ```typescript -check(input: unknown): ResultType; +get check(): (this: void, input: unknown) => ResultType; ``` -## Parameters - -| Parameter | Type | Description | -| --------- | ------- | ------------------ | -| input | unknown | the value to check | - -**Returns:** - -ResultType - ## Remarks When given a value that does not conform to the Type, throws an exception. diff --git a/markdown/types.basetypeimpl.is.md b/markdown/types.basetypeimpl.is.md index e7f41d9..633dcd0 100644 --- a/markdown/types.basetypeimpl.is.md +++ b/markdown/types.basetypeimpl.is.md @@ -2,22 +2,12 @@ [Home](./index.md) > [@skunkteam/types](./types.md) > [BaseTypeImpl](./types.basetypeimpl.md) > [is](./types.basetypeimpl.is.md) -## BaseTypeImpl.is() method +## BaseTypeImpl.is property A type guard for this Type. **Signature:** ```typescript -is(input: unknown): input is ResultType; +get is(): (this: void, input: Input) => input is unknown extends Input ? ResultType & Input : Input extends ResultType ? Input : never; ``` - -## Parameters - -| Parameter | Type | Description | -| --------- | ------- | ----------- | -| input | unknown | | - -**Returns:** - -input is ResultType diff --git a/markdown/types.basetypeimpl.md b/markdown/types.basetypeimpl.md index 7cf8ac4..784cf15 100644 --- a/markdown/types.basetypeimpl.md +++ b/markdown/types.basetypeimpl.md @@ -20,14 +20,16 @@ All type-implementations must extend this base class. Use [createType()](./types ## Properties -| Property | Modifiers | Type | Description | -| --------------------------------------------------------------------------- | -------------------------------------------------------- | --------------------------------------------------------------- | --------------------------------------------------------------------------- | -| [autoCast](./types.basetypeimpl.autocast.md) | readonly | this | The same type, but with an auto-casting default parser installed. | -| [autoCastAll](./types.basetypeimpl.autocastall.md) | readonly | this | Create a recursive autocasting version of the current type. | -| [basicType](./types.basetypeimpl.basictype.md) |

abstract

readonly

| [BasicType](./types.basictype.md) \| 'mixed' | The kind of values this type validates. | -| [enumerableLiteralDomain?](./types.basetypeimpl.enumerableliteraldomain.md) | readonly | Iterable<[LiteralValue](./types.literalvalue.md)> | _(Optional)_ The set of valid literals if enumerable. | -| [name](./types.basetypeimpl.name.md) |

abstract

readonly

| string | The name of the Type. | -| [typeConfig](./types.basetypeimpl.typeconfig.md) |

abstract

readonly

| TypeConfig | Extra information that is made available by this Type for runtime analysis. | +| Property | Modifiers | Type | Description | +| --------------------------------------------------------------------------- | -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------- | +| [autoCast](./types.basetypeimpl.autocast.md) | readonly | this | The same type, but with an auto-casting default parser installed. | +| [autoCastAll](./types.basetypeimpl.autocastall.md) | readonly | this | Create a recursive autocasting version of the current type. | +| [basicType](./types.basetypeimpl.basictype.md) |

abstract

readonly

| [BasicType](./types.basictype.md) \| 'mixed' | The kind of values this type validates. | +| [check](./types.basetypeimpl.check.md) | readonly | (this: void, input: unknown) => ResultType | Asserts that a value conforms to this Type and returns the input as is, if it does. | +| [enumerableLiteralDomain?](./types.basetypeimpl.enumerableliteraldomain.md) | readonly | Iterable<[LiteralValue](./types.literalvalue.md)> | _(Optional)_ The set of valid literals if enumerable. | +| [is](./types.basetypeimpl.is.md) | readonly | <Input>(this: void, input: Input) => input is unknown extends Input ? ResultType & Input : Input extends ResultType ? Input : never | A type guard for this Type. | +| [name](./types.basetypeimpl.name.md) |

abstract

readonly

| string | The name of the Type. | +| [typeConfig](./types.basetypeimpl.typeconfig.md) |

abstract

readonly

| TypeConfig | Extra information that is made available by this Type for runtime analysis. | ## Methods @@ -37,13 +39,11 @@ All type-implementations must extend this base class. Use [createType()](./types | [andThen(fn)](./types.basetypeimpl.andthen.md) | | Create a function with validated input. | | [assert(input)](./types.basetypeimpl.assert.md) | | Verifies that a value conforms to this Type. | | [autoCaster(this, value)?](./types.basetypeimpl.autocaster.md) | protected | _(Optional)_ The logic that is used in the autocasting version of the current type. | -| [check(input)](./types.basetypeimpl.check.md) | | Asserts that a value conforms to this Type and returns the input as is, if it does. | | [combineConfig(oldConfig, newConfig)](./types.basetypeimpl.combineconfig.md) | protected | Combine two config values into a new value. | | [construct(input)](./types.basetypeimpl.construct.md) | | Calls any registered parsers or auto-caster, verifies that the resulting value conforms to this Type and returns it if it does. | | [createAutoCastAllType()](./types.basetypeimpl.createautocastalltype.md) | protected | | | [createResult(input, result, validatorResult)](./types.basetypeimpl.createresult.md) | protected | Create a Result based on the given [ValidationResult](./types.validationresult.md). | | [extendWith(factory)](./types.basetypeimpl.extendwith.md) | | Extend the Type with additional static methods and properties. | -| [is(input)](./types.basetypeimpl.is.md) | | A type guard for this Type. | | [literal(input)](./types.basetypeimpl.literal.md) | | Calls any registered parsers or auto-caster, verifies that the resulting value conforms to this Type and returns it if it does. | | [or(\_other)](./types.basetypeimpl.or.md) | | Union this Type with another Type. | | [typeParser(input, options)?](./types.basetypeimpl.typeparser.md) | protected | _(Optional)_ Optional pre-processing parser. | diff --git a/src/base-type.test.ts b/src/base-type.test.ts index f38c646..2b9a012 100644 --- a/src/base-type.test.ts +++ b/src/base-type.test.ts @@ -1,3 +1,4 @@ +import assert from 'assert'; import { BaseTypeImpl } from './base-type.js'; import type { The } from './interfaces.js'; import { assignableTo, testTypes } from './testutils.js'; @@ -26,6 +27,27 @@ describe(BaseTypeImpl, () => { expect(value).toEqual({ key: 'value' }); } + const array = [value, value]; + if (array.every(string.is)) { + assignableTo<'a string'[]>(array); + assignableTo(['a string']); + expect(array).toEqual(['a string', 'a string']); + } else if (array.every(number.is)) { + assignableTo<123[]>(array); + assignableTo([123]); + expect(array).toEqual([123, 123]); + } else if (array.every(boolean.is)) { + assignableTo(array); + assignableTo([false]); + expect(array).toEqual([false, false]); + } else if (array.every(unknownRecord.is)) { + assignableTo<{ key: 'value' }[]>(array); + assignableTo([{ key: 'value' }]); + expect(array).toEqual([{ key: 'value' }, { key: 'value' }]); + } else { + assert.fail('should have matched one of the other predicates'); + } + testTypes(() => { string.assert(value); assignableTo<'a string'>(value); diff --git a/src/base-type.ts b/src/base-type.ts index a2dace3..f11c178 100644 --- a/src/base-type.ts +++ b/src/base-type.ts @@ -96,6 +96,8 @@ export abstract class BaseTypeImpl implements private readonly _instanceCache: { autoCast?: BaseTypeImpl; autoCastAll?: BaseTypeImpl; + boundCheck?: BaseTypeImpl['check']; + boundIs?: BaseTypeImpl['is']; } = {}; /** @@ -192,9 +194,11 @@ export abstract class BaseTypeImpl implements * * @param input - the value to check */ - check(input: unknown): ResultType { - this.assert(input); - return input; + get check(): (this: void, input: unknown) => ResultType { + return (this._instanceCache.boundCheck ??= input => { + this.assert(input); + return input; + }); } /** @@ -272,8 +276,15 @@ export abstract class BaseTypeImpl implements /** * A type guard for this Type. */ - is(input: unknown): input is ResultType { - return this.validate(input, { mode: 'check' }).ok; + get is(): ( + this: void, + input: Input, + ) => input is unknown extends Input ? ResultType & Input : Input extends ResultType ? Input : never { + this._instanceCache.boundIs ??= ( + input: Input, + ): input is unknown extends Input ? ResultType & Input : Input extends ResultType ? Input : never => + this.validate(input, { mode: 'check' }).ok; + return this._instanceCache.boundIs; } /** @@ -381,7 +392,7 @@ export abstract class BaseTypeImpl implements if (!baseResult.ok) { return type.createResult(input, undefined, baseResult.details); } - const tryResult = ValidationError.try( + const tryResult = ValidationError.try( { type, input }, // if no name is given, then default to the message "additional validation failed" () => validation(baseResult.value, options) || 'additional validation failed',