Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
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.

```typescript
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
4 changes: 2 additions & 2 deletions source/predicates/array.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ 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;

return this.addValidator({
Expand All @@ -156,6 +156,6 @@ export class ArrayPredicate<T = unknown> extends Predicate<T[]> {
return false;
}
}
});
}) as unknown as ArrayPredicate<U>;
Comment thread
mmkal marked this conversation as resolved.
Outdated
}
}
12 changes: 6 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,11 @@ export class ObjectPredicate extends Predicate<object> {
}));
```
*/
exactShape(shape: Shape) {
exactShape<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 => exact(object, shape)
});
}) as unknown as ObjectPredicate<TypeOfShape<S>>;
}
}
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
9 changes: 9 additions & 0 deletions source/utils/match-shape.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,15 @@ export interface Shape {
[key: string]: BasePredicate | Shape;
}

export type TypeOfShape<S extends BasePredicate | Shape> =
Comment thread
mmkal marked this conversation as resolved.
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
130 changes: 130 additions & 0 deletions test/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import test from 'ava';
import {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};
}>();
},

() => {
// For the rest of the validators, just make sure they're mapped to the correct type.
// This helper makes it easy to write a one-line type validation.
const ensure = <T>(predicate: BasePredicate<T>): T => {
ow(value, predicate);
return value;
};

expectTypeOf(ensure(ow.arrayBuffer)).toEqualTypeOf<ArrayBuffer>();
expectTypeOf(ensure(ow.buffer)).toEqualTypeOf<Buffer>();
expectTypeOf(ensure(ow.dataView)).toEqualTypeOf<DataView>();
expectTypeOf(ensure(ow.date)).toEqualTypeOf<Date>();
expectTypeOf(ensure(ow.error)).toEqualTypeOf<Error>();
expectTypeOf(ensure(ow.float32Array)).toEqualTypeOf<Float32Array>();
expectTypeOf(ensure(ow.float64Array)).toEqualTypeOf<Float64Array>();
expectTypeOf(ensure(ow.function)).toEqualTypeOf<Function>();
expectTypeOf(ensure(ow.int16Array)).toEqualTypeOf<Int16Array>();
expectTypeOf(ensure(ow.int32Array)).toEqualTypeOf<Int32Array>();
expectTypeOf(ensure(ow.map)).toEqualTypeOf<Map<unknown, unknown>>();
expectTypeOf(ensure(ow.int8Array)).toEqualTypeOf<Int8Array>();
expectTypeOf(ensure(ow.nan)).toEqualTypeOf(Number.NaN);
expectTypeOf(ensure(ow.null)).toEqualTypeOf<null>();
expectTypeOf(ensure(ow.nullOrUndefined)).toEqualTypeOf<null | undefined>();
expectTypeOf(ensure(ow.sharedArrayBuffer)).toEqualTypeOf<SharedArrayBuffer>();
expectTypeOf(ensure(ow.promise)).toEqualTypeOf<Promise<unknown>>();
expectTypeOf(ensure(ow.regExp)).toEqualTypeOf<RegExp>();
expectTypeOf(ensure(ow.set)).toEqualTypeOf<Set<any>>();
expectTypeOf(ensure(ow.symbol)).toEqualTypeOf<symbol>();
expectTypeOf(ensure(ow.typedArray)).toEqualTypeOf<TypedArray>();
expectTypeOf(ensure(ow.uint16Array)).toEqualTypeOf<Uint16Array>();
expectTypeOf(ensure(ow.uint32Array)).toEqualTypeOf<Uint32Array>();
expectTypeOf(ensure(ow.uint8Array)).toEqualTypeOf<Uint8Array>();
expectTypeOf(ensure(ow.uint8ClampedArray)).toEqualTypeOf<Uint8ClampedArray>();
expectTypeOf(ensure(ow.undefined)).toEqualTypeOf<undefined>();
expectTypeOf(ensure(ow.weakMap)).toEqualTypeOf<WeakMap<object, unknown>>();
expectTypeOf(ensure(ow.weakSet)).toEqualTypeOf<WeakSet<object>>();
}
];
}