Repository: sindresorhus/ow (ts)

Issue:
Tuple types in Ow?
What is the best way to check for the tuple type `[string, number]` in Ow?

This is the best that I can come up with:

```ts
// [string, number]
const predPairStringNumber = ow
  .object.instanceOf(Array)
  .exactShape({
    length: ow.number.equal(2),
    0: ow.string,
    1: ow.number,
  });
```

Pull request description:
Add `array.exactShape` predicate
exactShape implementation for array.

Fixes #179

Patch:
diff --git a/package.json b/package.json
index e3b929b..d39ced2 100644
--- a/package.json
+++ b/package.json
@@ -19,6 +19,7 @@
 		"docs": "typedoc source/index.ts",
 		"build": "del-cli dist && tsc",
 		"prepublishOnly": "npm run build",
+		"postpublish": "gh-pages --dist docs --no-history --message \"Deploy documentation\"",
 		"example": "npm run build && node example.js"
 	},
 	"files": [
@@ -61,6 +62,7 @@
 		"ava": "^2.4.0",
 		"del-cli": "^3.0.1",
 		"expect-type": "^0.11.0",
+		"gh-pages": "^3.1.0",
 		"nyc": "^15.1.0",
 		"ts-node": "^9.1.1",
 		"typedoc": "^0.20.13",
diff --git a/readme.md b/readme.md
index d3b14a2..36bb7c5 100644
--- a/readme.md
+++ b/readme.md
@@ -143,12 +143,12 @@ All the below types return a predicate. Every predicate has some extra operators
 
 #### Primitives
 
-- [`undefined`](https://sindresorhus.com/ow/interfaces/ow.html#undefined)
+- [`undefined`](https://sindresorhus.com/ow/interfaces/ow.html#undefined-1)
 - [`null`](https://sindresorhus.com/ow/interfaces/ow.html#null)
 - [`string`](https://sindresorhus.com/ow/classes/stringpredicate.html)
 - [`number`](https://sindresorhus.com/ow/classes/numberpredicate.html)
 - [`boolean`](https://sindresorhus.com/ow/classes/booleanpredicate.html)
-- [`symbol`](https://sindresorhus.com/ow/interfaces/ow.html#symbol)
+- [`symbol`](https://sindresorhus.com/ow/interfaces/ow.html#symbol-1)
 
 #### Built-in types
 
@@ -156,10 +156,10 @@ All the below types return a predicate. Every predicate has some extra operators
 - [`function`](https://sindresorhus.com/ow/interfaces/ow.html#function)
 - [`buffer`](https://sindresorhus.com/ow/interfaces/ow.html#buffer)
 - [`object`](https://sindresorhus.com/ow/classes/objectpredicate.html)
-- [`regExp`](https://sindresorhus.com/ow/interfaces/ow.html#regexp)
+- [`regExp`](https://sindresorhus.com/ow/interfaces/ow.html#regexp-1)
 - [`date`](https://sindresorhus.com/ow/classes/datepredicate.html)
 - [`error`](https://sindresorhus.com/ow/classes/errorpredicate.html)
-- [`promise`](https://sindresorhus.com/ow/interfaces/ow.html#promise)
+- [`promise`](https://sindresorhus.com/ow/interfaces/ow.html#promise-1)
 - [`map`](https://sindresorhus.com/ow/classes/mappredicate.html)
 - [`set`](https://sindresorhus.com/ow/classes/setpredicate.html)
 - [`weakMap`](https://sindresorhus.com/ow/classes/weakmappredicate.html)
@@ -168,12 +168,12 @@ All the below types return a predicate. Every predicate has some extra operators
 #### Typed arrays
 
 - [`int8Array`](https://sindresorhus.com/ow/interfaces/ow.html#int8Array)
-- [`uint8Array`](https://sindresorhus.com/ow/interfaces/ow.html#uint8Array)
-- [`uint8ClampedArray`](https://sindresorhus.com/ow/interfaces/ow.html#uint8ClampedArray)
+- [`uint8Array`](https://sindresorhus.com/ow/interfaces/ow.html#uint8Array-1)
+- [`uint8ClampedArray`](https://sindresorhus.com/ow/interfaces/ow.html#uint8ClampedArray-1)
 - [`int16Array`](https://sindresorhus.com/ow/interfaces/ow.html#int16Array)
-- [`uint16Array`](https://sindresorhus.com/ow/interfaces/ow.html#uint16Array)
+- [`uint16Array`](https://sindresorhus.com/ow/interfaces/ow.html#uint16Array-1)
 - [`int32Array`](https://sindresorhus.com/ow/interfaces/ow.html#in32Array)
-- [`uint32Array`](https://sindresorhus.com/ow/interfaces/ow.html#uin32Array)
+- [`uint32Array`](https://sindresorhus.com/ow/interfaces/ow.html#uin32Array-1)
 - [`float32Array`](https://sindresorhus.com/ow/interfaces/ow.html#float32Array)
 - [`float64Array`](https://sindresorhus.com/ow/interfaces/ow.html#float64Array)
 
@@ -181,13 +181,14 @@ All the below types return a predicate. Every predicate has some extra operators
 
 - [`arrayBuffer`](https://sindresorhus.com/ow/interfaces/ow.html#arraybuffer)
 - [`dataView`](https://sindresorhus.com/ow/interfaces/ow.html#dataview)
+- [`sharedArrayBuffer`](https://sindresorhus.com/ow/interfaces/ow.html#sharedarraybuffer-1)
 
 #### Miscellaneous
 
 - [`nan`](https://sindresorhus.com/ow/interfaces/ow.html#nan)
 - [`nullOrUndefined`](https://sindresorhus.com/ow/interfaces/ow.html#nullorundefined)
 - [`iterable`](https://sindresorhus.com/ow/interfaces/ow.html#iterable)
-- [`typedArray`](https://sindresorhus.com/ow/interfaces/ow.html#typedarray)
+- [`typedArray`](https://sindresorhus.com/ow/interfaces/ow.html#typedarray-1)
 
 ### Predicates
 
diff --git a/source/predicates/array.ts b/source/predicates/array.ts
index 2c5bc08..fc179e3 100644
--- a/source/predicates/array.ts
+++ b/source/predicates/array.ts
@@ -2,6 +2,8 @@ import isEqual = require('lodash.isequal');
 import {BasePredicate} from './base-predicate';
 import {Predicate, PredicateOptions} from './predicate';
 import ow from '..';
+import {Shape} from './object';
+import {exact} from '../utils/match-shape';
 
 export class ArrayPredicate<T = unknown> extends Predicate<T[]> {
 	/**
@@ -159,4 +161,23 @@ export class ArrayPredicate<T = unknown> extends Predicate<T[]> {
 			}
 		}) as ArrayPredicate<any>;
 	}
+
+	/**
+	Test if the elements in the array exactly matches the elements placed at the same indices in the predicates array.
+
+	@param predicates - Predicates to test the array against. Describes what the tested array should look like.
+
+	@example
+	```
+	ow(['1', 2], ow.array.exactShape([ow.string, ow.number]));
+	```
+	*/
+	exactShape(predicates: Predicate[]) {
+		const shape = predicates as unknown as Shape;
+
+		return this.addValidator({
+			message: (_, label, message) => `${message.replace('Expected', 'Expected element')} in ${label}`,
+			validator: object => exact(object, shape, undefined, true)
+		});
+	}
 }
diff --git a/source/utils/match-shape.ts b/source/utils/match-shape.ts
index 5c624ef..8890f03 100644
--- a/source/utils/match-shape.ts
+++ b/source/utils/match-shape.ts
@@ -76,7 +76,7 @@ Test if the `object` matches the `shape` exactly.
 @param shape - Shape to test the object against.
 @param parent - Name of the parent property.
 */
-export function exact(object: Record<string, any>, shape: Shape, parent?: string): boolean | string {
+export function exact(object: Record<string, any>, shape: Shape, parent?: string, isArray?: boolean): boolean | string {
 	const stack = generateStackTrace();
 	try {
 		const objectKeys = new Set<string>(Object.keys(object));
@@ -104,7 +104,7 @@ export function exact(object: Record<string, any>, shape: Shape, parent?: string
 		if (objectKeys.size > 0) {
 			const firstKey = [...objectKeys.keys()][0]!;
 			const label = parent ? `${parent}.${firstKey}` : firstKey;
-			return `Did not expect property \`${label}\` to exist, got \`${object[firstKey]}\``;
+			return `Did not expect ${isArray ? 'element' : 'property'} \`${label}\` to exist, got \`${object[firstKey]}\``;
 		}
 
 		return true;


Test patch:
diff --git a/test/array.ts b/test/array.ts
index a906829..bb71e57 100644
--- a/test/array.ts
+++ b/test/array.ts
@@ -168,3 +168,21 @@ test('array.ofType', t => {
 		ow(['foo', 'b'], 'foo', ow.array.ofType(ow.string.minLength(3)));
 	}, '(array `foo`) Expected string to have a minimum length of `3`, got `b`');
 });
+
+test('array.exactShape', t => {
+	t.notThrows(() => {
+		ow(['🦄', 2, 3, true, {isFirstCommit: true}], ow.array.exactShape([ow.string, ow.number, ow.number, ow.boolean, ow.object.exactShape({isFirstCommit: ow.boolean})]));
+	});
+
+	t.throws(() => {
+		ow(['🦄', 2, 'nope', true, {isFirstCommit: true}], ow.array.exactShape([ow.string, ow.number, ow.number, ow.boolean, ow.object.exactShape({isFirstCommit: ow.string})]));
+	}, 'Expected element `2` to be of type `number` but received type `string` in array');
+
+	t.throws(() => {
+		ow(['🦄', 'nope', {isFirstCommit: true}], ow.array.exactShape([ow.string, ow.string, ow.object.exactShape({isFirstCommit: ow.boolean}), ow.number, ow.boolean]));
+	}, 'Expected element `3` to be of type `number` but received type `undefined` in array');
+
+	t.throws(() => {
+		ow(['🦄', {isFirstCommit: true}, 'nope', 5, {accepted: false}], ow.array.exactShape([ow.string, ow.object.exactShape({isFirstCommit: ow.boolean}), ow.string]));
+	}, 'Did not expect element `3` to exist, got `5` in array');
+});