Skip to content

Commit

Permalink
Add asserts annotations (#194)
Browse files Browse the repository at this point in the history
  • Loading branch information
mmkal authored Dec 19, 2020
1 parent 72c1463 commit 66a4228
Show file tree
Hide file tree
Showing 9 changed files with 213 additions and 18 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
"@types/vali-date": "^1.0.0",
"ava": "^2.0.0",
"del-cli": "^3.0.1",
"expect-type": "^0.11.0",
"nyc": "^15.1.0",
"ts-node": "^9.0.0",
"typedoc": "^0.19.2",
Expand Down
12 changes: 11 additions & 1 deletion readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,17 @@ ow(unicorn, ow.object.exactShape({

[Complete API documentation](https://sindresorhus.com/ow)

Ow does not currently include TypeScript type guards, but we do [plan to include type assertions](https://github.com/sindresorhus/ow/issues/159).
Ow includes TypeScript type guards, so using it will narrow the type of previously-unknown values.

```ts
function (input: unknown) {
input.slice(0, 3) // Error, Property 'slice' does not exist on type 'unknown'

ow(input, ow.string)

input.slice(0, 3) // OK
}
```

### ow(value, predicate)

Expand Down
11 changes: 8 additions & 3 deletions source/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export interface Ow extends Modifiers, Predicates {
@param value - Value to test.
@param predicate - Predicate to test against.
*/
<T>(value: T, predicate: BasePredicate<T>): void;
<T>(value: unknown, predicate: BasePredicate<T>): asserts value is T;

/**
Test if `value` matches the provided `predicate`. Throws an `ArgumentError` with the specified `label` if the test fails.
Expand All @@ -43,7 +43,7 @@ export interface Ow extends Modifiers, Predicates {
@param label - Label which should be used in error messages.
@param predicate - Predicate to test against.
*/
<T>(value: T, label: string, predicate: BasePredicate<T>): void;
<T>(value: unknown, label: string, predicate: BasePredicate<T>): asserts value is T;
}

/**
Expand Down Expand Up @@ -103,7 +103,12 @@ Object.defineProperties(ow, {
}
});

export default predicates(modifiers(ow)) as Ow;
// Can't use `export default predicates(modifiers(ow)) as Ow` because the variable needs a type annotation to avoid a compiler error when used:
// Assertions require every name in the call target to be declared with an explicit type annotation.ts(2775)
// See https://github.com/microsoft/TypeScript/issues/36931 for more details.
const _ow: Ow = predicates(modifiers(ow)) as Ow;

export default _ow;

export {BasePredicate, Predicate};

Expand Down
9 changes: 8 additions & 1 deletion source/modifiers.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
import {BasePredicate} from '.';
import predicates, {Predicates} from './predicates';

type Optionalify<P> = P extends BasePredicate<infer X>
? P & BasePredicate<X | undefined>
: P;

export interface Modifiers {
/**
Make the following predicate optional so it doesn't fail when the value is `undefined`.
*/
readonly optional: Predicates;
readonly optional: {
[K in keyof Predicates]: Optionalify<Predicates[K]>
};
}

export default <T>(object: T): T & Modifiers => {
Expand Down
5 changes: 3 additions & 2 deletions source/predicates/array.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,9 +139,10 @@ export class ArrayPredicate<T = unknown> extends Predicate<T[]> {
ow(['a', 1], ow.array.ofType(ow.any(ow.string, ow.number)));
```
*/
ofType<P extends BasePredicate<T>>(predicate: P) {
ofType<U extends T>(predicate: BasePredicate<U>): ArrayPredicate<U> {
let error: string;

// TODO [typescript@>=5] If higher-kinded types are supported natively by typescript, refactor `addValidator` to use them to avoid the usage of `any`. Otherwise, bump or remove this TODO.
return this.addValidator({
message: (_, label) => `(${label}) ${error}`,
validator: value => {
Expand All @@ -156,6 +157,6 @@ export class ArrayPredicate<T = unknown> extends Predicate<T[]> {
return false;
}
}
});
}) as ArrayPredicate<any>;
}
}
13 changes: 7 additions & 6 deletions source/predicates/object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ import isEqual = require('lodash.isequal');
import hasItems from '../utils/has-items';
import ofType from '../utils/of-type';
import ofTypeDeep from '../utils/of-type-deep';
import {partial, exact, Shape} from '../utils/match-shape';
import {partial, exact, Shape, TypeOfShape} from '../utils/match-shape';
import {Predicate, PredicateOptions} from './predicate';
import {BasePredicate} from './base-predicate';

export {Shape};

export class ObjectPredicate extends Predicate<object> {
export class ObjectPredicate<T extends object = object> extends Predicate<T> {
/**
@hidden
*/
Expand Down Expand Up @@ -155,12 +155,12 @@ export class ObjectPredicate extends Predicate<object> {
}));
```
*/
partialShape(shape: Shape) {
partialShape<S extends Shape = Shape>(shape: S): ObjectPredicate<TypeOfShape<S>> {
return this.addValidator({
// TODO: Improve this when message handling becomes smarter
message: (_, label, message) => `${message.replace('Expected', 'Expected property')} in ${label}`,
validator: object => partial(object, shape)
});
}) as unknown as ObjectPredicate<TypeOfShape<S>>;
}

/**
Expand All @@ -179,11 +179,12 @@ export class ObjectPredicate extends Predicate<object> {
}));
```
*/
exactShape(shape: Shape) {
exactShape<S extends Shape = Shape>(shape: S): ObjectPredicate<TypeOfShape<S>> {
// TODO [typescript@>=5] If higher-kinded types are supported natively by typescript, refactor `addValidator` to use them to avoid the usage of `any`. Otherwise, bump or remove this TODO.
return this.addValidator({
// TODO: Improve this when message handling becomes smarter
message: (_, label, message) => `${message.replace('Expected', 'Expected property')} in ${label}`,
validator: object => exact(object, shape)
});
}) as ObjectPredicate<any>;
}
}
8 changes: 3 additions & 5 deletions source/predicates/predicate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,15 +95,13 @@ export class Predicate<T = unknown> implements BasePredicate<T> {
/**
@hidden
*/
[testSymbol](value: T | undefined, main: Main, label: string | Function): asserts value {
[testSymbol](value: T, main: Main, label: string | Function): asserts value is T {
for (const {validator, message} of this.context.validators) {
if (this.options.optional === true && value === undefined) {
continue;
}

const knownValue = value!;

const result = validator(knownValue);
const result = validator(value);

if (result === true) {
continue;
Expand All @@ -120,7 +118,7 @@ export class Predicate<T = unknown> implements BasePredicate<T> {
this.type;

// TODO: Modify the stack output to show the original `ow()` call instead of this `throw` statement
throw new ArgumentError(message(knownValue, label2, result), main);
throw new ArgumentError(message(value, label2, result), main);
}
}

Expand Down
26 changes: 26 additions & 0 deletions source/utils/match-shape.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,32 @@ export interface Shape {
[key: string]: BasePredicate | Shape;
}

/**
Extracts a regular type from a shape definition.
@example
```
const myShape = {
foo: ow.string,
bar: {
baz: ow.boolean
}
}
type X = TypeOfShape<typeof myShape> // {foo: string; bar: {baz: boolean}}
```
This is used in the `ow.object.partialShape(…)` and `ow.object.exactShape(…)` functions.
*/
export type TypeOfShape<S extends BasePredicate | Shape> =
S extends BasePredicate<infer X>
? X
: S extends Shape
? {
[K in keyof S]: TypeOfShape<S[K]>
}
: never;

/**
Test if the `object` matches the `shape` partially.
Expand Down
146 changes: 146 additions & 0 deletions test/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import test from 'ava';
import {ExpectTypeOf, expectTypeOf} from 'expect-type';
import {TypedArray} from 'type-fest';
import ow, {BasePredicate} from '../source';

test('type-level tests', t => {
t.is(typeof typeTests, 'function');
});

// These tests will fail at compile-time, not runtime.
// The function isn't actually called, it's just a way of declaring scoped type-level tests
// that doesn't make the compiler angry about unused variables.
function typeTests(value: unknown) {
return [
() => {
expectTypeOf(value).toBeUnknown();

ow(value, ow.string);

expectTypeOf(value).toBeString();
expectTypeOf(value).not.toBeNever();

ow(value, ow.boolean);

expectTypeOf(value).toBeNever(); // Can't be a string and a boolean!
},

() => {
ow(value, 'my-label', ow.number);

expectTypeOf(value).toBeNumber();
},

() => {
ow(value, ow.string.maxLength(7));

expectTypeOf(value).toBeString();
},

() => {
ow(value, ow.optional.string);

expectTypeOf(value).toEqualTypeOf<string | undefined>();
},

() => {
ow(value, ow.iterable);

expectTypeOf(value).toEqualTypeOf<Iterable<unknown>>();

ow(value, ow.array);

expectTypeOf(value).toEqualTypeOf<unknown[]>();

ow(value, ow.array.ofType(ow.string));

expectTypeOf(value).toEqualTypeOf<string[]>();
},

() => {
ow(value, ow.array.ofType(ow.any(ow.string, ow.number, ow.boolean, ow.nullOrUndefined)));

expectTypeOf(value).toEqualTypeOf<Array<string | number | boolean | null | undefined>>();
},

() => {
ow(value, ow.object);

expectTypeOf(value).toBeObject();

ow(value, ow.object.partialShape({
foo: ow.string
}));

expectTypeOf(value).toEqualTypeOf<{
foo: string;
}>();

ow(value, ow.object.exactShape({
foo: ow.string,
bar: ow.object.exactShape({
baz: ow.number
})
}));

expectTypeOf(value).toEqualTypeOf<{
foo: string;
bar: {baz: number};
}>();
},

() => {
// To make sure all validators are mapped to the correct type, create a `Tests` type which requires that
// every property of `ow` has its type-mapping explicitly tested. If more properties are added this will
// fail until a type assertion is added below.

type AssertionProps = Exclude<keyof typeof ow, 'any' | 'isValid' | 'create' | 'optional'>;

type Tests = {
[K in AssertionProps]:
typeof ow[K] extends BasePredicate<infer T>
? (type: ExpectTypeOf<T, true>) => void
: never
};

const tests: Tests = {
array: expect => expect.toBeArray(),
arrayBuffer: expect => expect.toEqualTypeOf<ArrayBuffer>(),
boolean: expect => expect.toBeBoolean(),
buffer: expect => expect.toEqualTypeOf<Buffer>(),
dataView: expect => expect.toEqualTypeOf<DataView>(),
date: expect => expect.toEqualTypeOf<Date>(),
error: expect => expect.toEqualTypeOf<Error>(),
float32Array: expect => expect.toEqualTypeOf<Float32Array>(),
float64Array: expect => expect.toEqualTypeOf<Float64Array>(),
function: expect => expect.toEqualTypeOf<Function>(),
int16Array: expect => expect.toEqualTypeOf<Int16Array>(),
int32Array: expect => expect.toEqualTypeOf<Int32Array>(),
int8Array: expect => expect.toEqualTypeOf<Int8Array>(),
iterable: expect => expect.toEqualTypeOf<Iterable<unknown>>(),
map: expect => expect.toEqualTypeOf<Map<unknown, unknown>>(),
nan: expect => expect.toEqualTypeOf(Number.NaN),
null: expect => expect.toEqualTypeOf<null>(),
nullOrUndefined: expect => expect.toEqualTypeOf<null | undefined>(),
number: expect => expect.toBeNumber(),
object: expect => expect.toBeObject(),
promise: expect => expect.toEqualTypeOf<Promise<unknown>>(),
regExp: expect => expect.toEqualTypeOf<RegExp>(),
set: expect => expect.toEqualTypeOf<Set<any>>(),
sharedArrayBuffer: expect => expect.toEqualTypeOf<SharedArrayBuffer>(),
string: expect => expect.toBeString(),
symbol: expect => expect.toEqualTypeOf<symbol>(),
typedArray: expect => expect.toEqualTypeOf<TypedArray>(),
uint16Array: expect => expect.toEqualTypeOf<Uint16Array>(),
uint32Array: expect => expect.toEqualTypeOf<Uint32Array>(),
uint8Array: expect => expect.toEqualTypeOf<Uint8Array>(),
uint8ClampedArray: expect => expect.toEqualTypeOf<Uint8ClampedArray>(),
undefined: expect => expect.toEqualTypeOf<undefined>(),
weakMap: expect => expect.toEqualTypeOf<WeakMap<object, unknown>>(),
weakSet: expect => expect.toEqualTypeOf<WeakSet<object>>()
};

return tests;
}
];
}

0 comments on commit 66a4228

Please sign in to comment.