Repository: sindresorhus/ow (ts)

Issue:
Bug: `isValid` wrong typing
`isValid` always assumes the type of the value, because the first argument should be `unknown` (like it is for `ow` itself): https://github.com/sindresorhus/ow/blob/3728d9119a19e1a95cb58bebe602afd308c40630/source/index.ts#L39

Looking over the rest of the file, it seems this mistake was made in several places; `ReusableValidator` should probably also not use `T` for the value (and assert?)

Pull request:
Fix input type on some methods and add `AssertingValidator`
Fixes #222 

- Some functions incorrectly used `value: T` instead of `value: unknown` (want to `assert value is T`, not assume it)
- Added type to make `ReusableValidator` also `assert value is T`
- Convenience type to infer nested `BasePredicate` for `object.partialShape` (type-check that type and predicate are equal)

I am not fully sure about the last two points, suggestions welcome.

Patch:
diff --git a/source/index.ts b/source/index.ts
index 54ba1b5..88f5d3b 100644
--- a/source/index.ts
+++ b/source/index.ts
@@ -52,7 +52,7 @@ export interface Ow extends Modifiers, Predicates {
 	@param value - Value to test.
 	@param predicate - Predicate to test against.
 	*/
-	isValid: <T>(value: T, predicate: BasePredicate<T>) => value is T;
+	isValid: <T>(value: unknown, predicate: BasePredicate<T>) => value is T;
 
 	/**
 	Create a reusable validator.
@@ -73,10 +73,32 @@ export interface ReusableValidator<T> {
 	@param label - Override the label which should be used in error messages.
 	*/
 	// eslint-disable-next-line @typescript-eslint/prefer-function-type
-	(value: T, label?: string): void;
+	(value: unknown | T, label?: string): void;
 }
 
-const ow = <T>(value: T, labelOrPredicate: unknown, predicate?: BasePredicate<T>): void => {
+/**
+Turn a `ReusableValidator` into one with a type assertion.
+
+@example
+```
+const checkUsername = ow.create(ow.string.minLength(3));
+const checkUsername_: AssertingValidator<typeof checkUsername> = checkUsername;
+```
+
+@example
+```
+const predicate = ow.string.minLength(3);
+const checkUsername: AssertingValidator<typeof predicate> = ow.create(predicate);
+```
+*/
+export type AssertingValidator<T> =
+	T extends ReusableValidator<infer R>
+		? (value: unknown, label?: string) => asserts value is R
+		: T extends BasePredicate<infer R>
+			? (value: unknown, label?: string) => asserts value is R
+			: never;
+
+const ow = <T>(value: unknown, labelOrPredicate: unknown, predicate?: BasePredicate<T>): void => {
 	if (!isPredicate(labelOrPredicate) && typeof labelOrPredicate !== 'string') {
 		throw new TypeError(`Expected second argument to be a predicate or a string, got \`${typeof labelOrPredicate}\``);
 	}
@@ -95,7 +117,7 @@ const ow = <T>(value: T, labelOrPredicate: unknown, predicate?: BasePredicate<T>
 
 Object.defineProperties(ow, {
 	isValid: {
-		value: <T>(value: T, predicate: BasePredicate<T>): boolean => {
+		value: <T>(value: unknown, predicate: BasePredicate<T>): boolean => {
 			try {
 				test(value, '', predicate);
 				return true;
@@ -105,7 +127,7 @@ Object.defineProperties(ow, {
 		}
 	},
 	create: {
-		value: <T>(labelOrPredicate: BasePredicate<T> | string | undefined, predicate?: BasePredicate<T>) => (value: T, label?: string): void => {
+		value: <T>(labelOrPredicate: BasePredicate<T> | string | undefined, predicate?: BasePredicate<T>) => (value: unknown, label?: string): asserts value is T => {
 			if (isPredicate(labelOrPredicate)) {
 				const stackFrames = callsites();
 


Test patch:
diff --git a/test/test.ts b/test/test.ts
index 506a8fa..ed42719 100644
--- a/test/test.ts
+++ b/test/test.ts
@@ -1,5 +1,5 @@
 import test from 'ava';
-import ow, {ArgumentError} from '../source';
+import ow, {ArgumentError, AssertingValidator} from '../source';
 import {createAnyError} from './fixtures/create-error';
 
 test('not', t => {
@@ -284,15 +284,18 @@ test('isValid', t => {
 	t.true(ow.isValid('foo!', ow.string.not.alphanumeric));
 	t.true(ow.isValid('foo!', ow.any(ow.string, ow.number)));
 	t.true(ow.isValid(1, ow.any(ow.string, ow.number)));
-	t.false(ow.isValid(1 as any, ow.string));
-	t.false(ow.isValid(1 as any, ow.number.greaterThan(2)));
-	t.false(ow.isValid(true as any, ow.any(ow.string, ow.number)));
+	t.false(ow.isValid(1, ow.string));
+	t.false(ow.isValid(1, ow.number.greaterThan(2)));
+	t.false(ow.isValid(true, ow.any(ow.string, ow.number)));
 });
 
 test('reusable validator', t => {
 	const checkUsername = ow.create(ow.string.minLength(3));
+	const checkUsername_: AssertingValidator<typeof checkUsername> = checkUsername;
+	const checkUsername__: AssertingValidator<typeof ow.string> = checkUsername;
 
 	const value = 'x';
+	const value_ = 'foo' as string | number;
 
 	t.notThrows(() => {
 		checkUsername('foo');
@@ -302,6 +305,16 @@ test('reusable validator', t => {
 		checkUsername('foobar');
 	});
 
+	t.notThrows(() => {
+		checkUsername_(value_);
+		((_: string): void => {})(value_); // The _ function should narrow the type. If it didn't, this would not compile
+	});
+
+	t.notThrows(() => {
+		checkUsername__(value_);
+		((_: string): void => {})(value_); // The __ function should narrow the type. If it didn't, this would not compile
+	});
+
 	t.throws(() => {
 		checkUsername('fo');
 	}, 'Expected string to have a minimum length of `3`, got `fo`');
@@ -311,7 +324,7 @@ test('reusable validator', t => {
 	}, 'Expected string to have a minimum length of `3`, got `x`');
 
 	const error = t.throws<ArgumentError>(() => {
-		checkUsername(5 as any);
+		checkUsername(5);
 	}, [
 		'Expected argument to be of type `string` but received type `number`',
 		'Expected string to have a minimum length of `3`, got `5`'
@@ -352,7 +365,7 @@ test('reusable validator called with label', t => {
 	}, 'Expected string `bar` to have a minimum length of `3`, got `x`');
 
 	const error = t.throws<ArgumentError>(() => {
-		checkUsername(5 as any, label);
+		checkUsername(5, label);
 	}, [
 		'Expected `bar` to be of type `string` but received type `number`',
 		'Expected string `bar` to have a minimum length of `3`, got `5`'
@@ -386,7 +399,7 @@ test('reusable validator with label', t => {
 	}, 'Expected string `foo` to have a minimum length of `3`, got `fo`');
 
 	const error = t.throws<ArgumentError>(() => {
-		checkUsername(5 as any);
+		checkUsername(5);
 	}, [
 		'Expected `foo` to be of type `string` but received type `number`',
 		'Expected string `foo` to have a minimum length of `3`, got `5`'
@@ -422,7 +435,7 @@ test('reusable validator with label called with label', t => {
 	}, 'Expected string `bar` to have a minimum length of `3`, got `fo`');
 
 	const error = t.throws<ArgumentError>(() => {
-		checkUsername(5 as any, label);
+		checkUsername(5, label);
 	}, [
 		'Expected `bar` to be of type `string` but received type `number`',
 		'Expected string `bar` to have a minimum length of `3`, got `5`'
@@ -459,7 +472,7 @@ test('any-reusable validator', t => {
 	));
 
 	t.throws(() => {
-		checkUsername(5 as any);
+		checkUsername(5);
 	}, createAnyError(
 		'Expected argument to be of type `string` but received type `number`',
 		'Expected string to include `.`, got `5`',