Skip to content
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
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;
}
];
}