Skip to content

Commit 474b902

Browse files
committed
feat(shape): added additionalProperties arg
1 parent c199dae commit 474b902

2 files changed

Lines changed: 57 additions & 24 deletions

File tree

src/predicate/factory/shape.ts

Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
/* eslint-disable @typescript-eslint/ban-types */
2+
import { hasAdditionalProperties } from '../../hasAdditionalProperties.js';
23
import { isAnyObject } from '../isAnyObject.js';
34

45
import { literal } from './literal.js';
@@ -40,24 +41,26 @@ export type InferTypeFromShape<Shape extends ObjectShapeRecord<object>> = {
4041

4142
export const shape = <Type extends object, Shape extends ObjectShapeRecord<Type> = ObjectShapeRecord<Type>>(
4243
objectShape: Shape,
44+
additionalProperties = true,
4345
): TypePredicateFn<Pretty<Type & InferTypeFromShape<Shape>>> => {
44-
const entries: [key: string | symbol, predicate: TypePredicateFn<unknown>][] = Reflect.ownKeys(objectShape).map(
45-
(key) => {
46-
const value = Reflect.get(objectShape, key);
47-
48-
const predicate =
49-
typeof value === 'function'
50-
? (value as TypePredicateFn<unknown>)
51-
: value instanceof RegExp
52-
? matching(value)
53-
: isAnyObject(value)
54-
? shape(value)
55-
: literal(value);
56-
57-
return [key, predicate];
58-
},
59-
);
46+
const knownProperties = Reflect.ownKeys(objectShape);
47+
const entries: [key: string | symbol, predicate: TypePredicateFn<unknown>][] = knownProperties.map((key) => {
48+
const value = Reflect.get(objectShape, key);
49+
50+
const predicate =
51+
typeof value === 'function'
52+
? (value as TypePredicateFn<unknown>)
53+
: value instanceof RegExp
54+
? matching(value)
55+
: isAnyObject(value)
56+
? shape(value)
57+
: literal(value);
58+
59+
return [key, predicate];
60+
});
6061

6162
return (input: unknown): input is Pretty<Type & InferTypeFromShape<Shape>> =>
62-
isAnyObject(input) && entries.every(([key, predicate]) => predicate(Reflect.get(input, key)));
63+
isAnyObject(input) &&
64+
entries.every(([key, predicate]) => predicate(Reflect.get(input, key))) &&
65+
(additionalProperties || !hasAdditionalProperties(input, knownProperties));
6366
};

test/predicate/factory/shape.test.ts

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@ describe('shape()', () => {
2323

2424
type User = {
2525
name: Name;
26-
title: string;
26+
job: {
27+
title: string;
28+
};
2729
role: Role;
2830
value: number;
2931
age?: number;
@@ -39,7 +41,9 @@ describe('shape()', () => {
3941

4042
const userShape = {
4143
name: isName,
42-
title: /software/i,
44+
job: {
45+
title: /\bsoftware\b/i,
46+
},
4347
role: Role.User,
4448
value: range(0, 100),
4549
[valueSymbol]: range(0, 100_000),
@@ -53,19 +57,21 @@ describe('shape()', () => {
5357
tuple: ['PI', Math.PI],
5458
} satisfies UserShape;
5559

56-
const fn = shape(userShape);
57-
5860
it('Returns a type predicate function', () => {
59-
expect(fn).instanceOf(Function);
61+
expect(shape(userShape)).instanceOf(Function);
6062
});
6163

6264
it('Checks string and symbol properties', () => {
65+
const fn = shape(userShape);
66+
6367
expect(fn({})).toBeFalsy();
6468

6569
expect(
6670
fn({
6771
name: 'Test Testerson',
68-
title: 'Software Engineer',
72+
job: {
73+
title: 'Software Engineer',
74+
},
6975
role: Role.User,
7076
value: 100,
7177
[valueSymbol]: 100_000,
@@ -78,7 +84,9 @@ describe('shape()', () => {
7884
expect(
7985
fn({
8086
name: 'Test Testerson',
81-
title: 'Senior Software Engineer',
87+
job: {
88+
title: 'Senior Software Engineer',
89+
},
8290
role: Role.User,
8391
value: 100,
8492
[valueSymbol]: 100_000,
@@ -91,4 +99,26 @@ describe('shape()', () => {
9199
}),
92100
).toBeTruthy();
93101
});
102+
103+
it('Can check for additional properties', () => {
104+
const fn = shape(
105+
{
106+
name: isString,
107+
},
108+
false,
109+
);
110+
111+
expect(
112+
fn({
113+
name: 'Test Testerson',
114+
}),
115+
).toBeTruthy();
116+
117+
expect(
118+
fn({
119+
name: 'Test Testerson',
120+
extra: 'not allowed',
121+
}),
122+
).toBeFalsy();
123+
});
94124
});

0 commit comments

Comments
 (0)