Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: type-guards are bound to the type-instance #67

Merged
merged 2 commits into from
Aug 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
115 changes: 114 additions & 1 deletion src/base-type.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import assert from 'assert';
import { BaseTypeImpl } from './base-type.js';
import type { The } from './interfaces.js';
import { assignableTo, testTypes } from './testutils.js';
import { boolean, int, number, object, pattern, string, unknownRecord } from './types/index.js';
import { boolean, int, literal, number, object, pattern, string, undefinedType, unknownRecord } from './types/index.js';

describe(BaseTypeImpl, () => {
test.each(['a string', 123, false, { key: 'value' }] as const)('guard value: %p', value => {
Expand All @@ -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 All @@ -51,6 +73,97 @@ describe(BaseTypeImpl, () => {
});
});

test('guard value: unknown', () => {
const value = undefined as unknown;
if (string.is(value)) {
assignableTo<string>(value);
assignableTo<typeof value>('a string');
}
if (number.is(value)) {
assignableTo<number>(value);
assignableTo<typeof value>(123);
}
if (boolean.is(value)) {
assignableTo<boolean>(value);
assignableTo<typeof value>(false);
}
if (unknownRecord.is(value)) {
assignableTo<unknownRecord>(value);
assignableTo<typeof value>({ key: 'value' });
}
expect(undefinedType.is(value)).toBeTrue();
if (undefinedType.is(value)) {
assignableTo<undefined>(value);
assignableTo<typeof value>(undefined);
}

const array = [value, value];
if (array.every(string.is)) {
assignableTo<string[]>(array);
assignableTo<typeof array>(['a string']);
} else if (array.every(number.is)) {
assignableTo<number[]>(array);
assignableTo<typeof array>([123]);
} else if (array.every(boolean.is)) {
assignableTo<boolean[]>(array);
assignableTo<typeof array>([false]);
} else if (array.every(unknownRecord.is)) {
assignableTo<unknownRecord[]>(array);
assignableTo<typeof array>([{ key: 'value' }]);
} else if (array.every(undefinedType.is)) {
assignableTo<undefined[]>(array);
assignableTo<typeof array>([undefined]);
} else {
assert.fail('should have matched the last predicate');
}

testTypes(() => {
string.assert(value);
assignableTo<string>(value);
assignableTo<typeof value>('a string');
});

testTypes(() => {
number.assert(value);
assignableTo<number>(value);
assignableTo<typeof value>(123);
});

testTypes(() => {
boolean.assert(value);
assignableTo<boolean>(value);
assignableTo<typeof value>(false);
});

testTypes(() => {
unknownRecord.assert(value);
assignableTo<unknownRecord>(value);
assignableTo<typeof value>({ key: 'value' });
});
});

test('guard objects', () => {
type TheType = The<typeof TheType>;
const TheType = object('TheType', { narrow: literal('value'), wide: string });

type UnionOfObjects =
| { narrow: number; wide: string } // not compatible because of narrow: number
| { narrow: string; wide: 'sneaky narrow' } // compatible if narrow happens to be `'value'`
| { something: 'else' }; // compatible if narrow and wide are present, we don't know.
const obj = { narrow: 'value', wide: 'sneaky narrow' } as UnionOfObjects;
if (TheType.is(obj)) {
assignableTo<{ narrow: 'value'; wide: 'sneaky narrow' } | { something: 'else'; narrow: 'value'; wide: string }>(obj);
assignableTo<typeof obj>({ narrow: 'value', wide: 'sneaky narrow' });
assignableTo<typeof obj>({ something: 'else', narrow: 'value', wide: 'something else' });
}

const value = { narrow: 'value', wide: 'a literal' as const };
if (TheType.is(value)) {
assignableTo<{ narrow: 'value'; wide: 'a literal' }>(value);
assignableTo<typeof value>({ narrow: 'value', wide: 'a literal' });
}
});

test('#literal', () => {
type NumericString = The<typeof NumericString>;
const NumericString = pattern('NumericString', /^\d+$/);
Expand Down
Loading