Skip to content

Commit

Permalink
feat: type-guards are bound to the type-instance
Browse files Browse the repository at this point in the history
  • Loading branch information
pavadeli committed Jun 26, 2023
1 parent 61240cd commit e06f267
Show file tree
Hide file tree
Showing 8 changed files with 61 additions and 47 deletions.
3 changes: 2 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,6 @@
"etc/**/*": true,
"markdown/**/*": true,
"node_modules/**/*": true
}
},
"task.allowAutomaticTasks": "on"
}
4 changes: 2 additions & 2 deletions etc/types.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,15 +62,15 @@ export abstract class BaseTypeImpl<ResultType, TypeConfig = unknown> implements
get autoCastAll(): this;
protected autoCaster?(this: BaseTypeImpl<ResultType, TypeConfig>, 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)
protected createAutoCastAllType(): this;
protected createResult(input: unknown, result: unknown, validatorResult: ValidationResult): Result<ResultType>;
readonly enumerableLiteralDomain?: Iterable<LiteralValue>;
extendWith<E>(factory: (type: this) => E): this & E;
is(input: unknown): input is ResultType;
get is(): <Input>(this: void, input: Input) => input is unknown extends Input ? ResultType & Input : Input extends ResultType ? Input : never;
literal(input: DeepUnbranded<ResultType>): ResultType;
abstract readonly name: string;
or<Other>(_other: BaseTypeImpl<Other, any>): Type<ResultType | Other>;
Expand Down
8 changes: 4 additions & 4 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'] },
Expand Down
14 changes: 2 additions & 12 deletions markdown/types.basetypeimpl.check.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,16 @@

[Home](./index.md) &gt; [@skunkteam/types](./types.md) &gt; [BaseTypeImpl](./types.basetypeimpl.md) &gt; [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.
Expand Down
14 changes: 2 additions & 12 deletions markdown/types.basetypeimpl.is.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,12 @@

[Home](./index.md) &gt; [@skunkteam/types](./types.md) &gt; [BaseTypeImpl](./types.basetypeimpl.md) &gt; [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(): <Input>(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
20 changes: 10 additions & 10 deletions markdown/types.basetypeimpl.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) | <code>readonly</code> | this | The same type, but with an auto-casting default parser installed. |
| [autoCastAll](./types.basetypeimpl.autocastall.md) | <code>readonly</code> | this | Create a recursive autocasting version of the current type. |
| [basicType](./types.basetypeimpl.basictype.md) | <p><code>abstract</code></p><p><code>readonly</code></p> | [BasicType](./types.basictype.md) \| 'mixed' | The kind of values this type validates. |
| [enumerableLiteralDomain?](./types.basetypeimpl.enumerableliteraldomain.md) | <code>readonly</code> | Iterable&lt;[LiteralValue](./types.literalvalue.md)<!-- -->&gt; | _(Optional)_ The set of valid literals if enumerable. |
| [name](./types.basetypeimpl.name.md) | <p><code>abstract</code></p><p><code>readonly</code></p> | string | The name of the Type. |
| [typeConfig](./types.basetypeimpl.typeconfig.md) | <p><code>abstract</code></p><p><code>readonly</code></p> | TypeConfig | Extra information that is made available by this Type for runtime analysis. |
| Property | Modifiers | Type | Description |
| --------------------------------------------------------------------------- | -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------- |
| [autoCast](./types.basetypeimpl.autocast.md) | <code>readonly</code> | this | The same type, but with an auto-casting default parser installed. |
| [autoCastAll](./types.basetypeimpl.autocastall.md) | <code>readonly</code> | this | Create a recursive autocasting version of the current type. |
| [basicType](./types.basetypeimpl.basictype.md) | <p><code>abstract</code></p><p><code>readonly</code></p> | [BasicType](./types.basictype.md) \| 'mixed' | The kind of values this type validates. |
| [check](./types.basetypeimpl.check.md) | <code>readonly</code> | (this: void, input: unknown) =&gt; ResultType | Asserts that a value conforms to this Type and returns the input as is, if it does. |
| [enumerableLiteralDomain?](./types.basetypeimpl.enumerableliteraldomain.md) | <code>readonly</code> | Iterable&lt;[LiteralValue](./types.literalvalue.md)<!-- -->&gt; | _(Optional)_ The set of valid literals if enumerable. |
| [is](./types.basetypeimpl.is.md) | <code>readonly</code> | &lt;Input&gt;(this: void, input: Input) =&gt; input is unknown extends Input ? ResultType &amp; Input : Input extends ResultType ? Input : never | A type guard for this Type. |
| [name](./types.basetypeimpl.name.md) | <p><code>abstract</code></p><p><code>readonly</code></p> | string | The name of the Type. |
| [typeConfig](./types.basetypeimpl.typeconfig.md) | <p><code>abstract</code></p><p><code>readonly</code></p> | TypeConfig | Extra information that is made available by this Type for runtime analysis. |
## Methods
Expand All @@ -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) | <code>protected</code> | _(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) | <code>protected</code> | 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) | <code>protected</code> | |
| [createResult(input, result, validatorResult)](./types.basetypeimpl.createresult.md) | <code>protected</code> | 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) | <code>protected</code> | _(Optional)_ Optional pre-processing parser. |
Expand Down
22 changes: 22 additions & 0 deletions src/base-type.test.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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<typeof array>(['a string']);
expect(array).toEqual(['a string', 'a string']);
} else if (array.every(number.is)) {
assignableTo<123[]>(array);
assignableTo<typeof array>([123]);
expect(array).toEqual([123, 123]);
} else if (array.every(boolean.is)) {
assignableTo<false[]>(array);
assignableTo<typeof array>([false]);
expect(array).toEqual([false, false]);
} else if (array.every(unknownRecord.is)) {
assignableTo<{ key: 'value' }[]>(array);
assignableTo<typeof array>([{ 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);
Expand Down
23 changes: 17 additions & 6 deletions src/base-type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,8 @@ export abstract class BaseTypeImpl<ResultType, TypeConfig = unknown> implements
private readonly _instanceCache: {
autoCast?: BaseTypeImpl<ResultType, TypeConfig>;
autoCastAll?: BaseTypeImpl<ResultType, TypeConfig>;
boundCheck?: BaseTypeImpl<ResultType, TypeConfig>['check'];
boundIs?: BaseTypeImpl<ResultType, TypeConfig>['is'];
} = {};

/**
Expand Down Expand Up @@ -192,9 +194,11 @@ export abstract class BaseTypeImpl<ResultType, TypeConfig = unknown> 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;
});
}

/**
Expand Down Expand Up @@ -272,8 +276,15 @@ export abstract class BaseTypeImpl<ResultType, TypeConfig = unknown> implements
/**
* A type guard for this Type.
*/
is(input: unknown): input is ResultType {
return this.validate(input, { mode: 'check' }).ok;
get is(): <Input>(
this: void,
input: Input,
) => input is unknown extends Input ? ResultType & Input : Input extends ResultType ? Input : never {
this._instanceCache.boundIs ??= <Input>(
input: Input,
): input is unknown extends Input ? ResultType & Input : Input extends ResultType ? Input : never =>
this.validate(input, { mode: 'check' }).ok;
return this._instanceCache.boundIs;
}

/**
Expand Down Expand Up @@ -381,7 +392,7 @@ export abstract class BaseTypeImpl<ResultType, TypeConfig = unknown> implements
if (!baseResult.ok) {
return type.createResult(input, undefined, baseResult.details);
}
const tryResult = ValidationError.try(
const tryResult = ValidationError.try<ValidationResult>(
{ type, input },
// if no name is given, then default to the message "additional validation failed"
() => validation(baseResult.value, options) || 'additional validation failed',
Expand Down

0 comments on commit e06f267

Please sign in to comment.