Skip to content

Commit 3626407

Browse files
committed
👍 Add isObjectOf<T>
1 parent 349a6f6 commit 3626407

File tree

2 files changed

+99
-0
lines changed

2 files changed

+99
-0
lines changed

is.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,58 @@ export function isRecordOf<T>(
9898
isRecord(x) && Object.values(x).every(pred);
9999
}
100100

101+
type FlatType<T> = T extends RecordOf<unknown>
102+
? { [K in keyof T]: FlatType<T[K]> }
103+
: T;
104+
105+
export type ObjectOf<
106+
T extends RecordOf<Predicate<unknown>>,
107+
> = FlatType<{ [K in keyof T]: T[K] extends Predicate<infer U> ? U : never }>;
108+
109+
/**
110+
* Return a type predicate function that returns `true` if the type of `x` is `ObjectOf<T>`.
111+
* When `options.strict` is `true`, the number of keys of `x` must be equal to the number of keys of `predObj`.
112+
* Otherwise, the number of keys of `x` must be greater than or equal to the number of keys of `predObj`.
113+
*
114+
* ```ts
115+
* import is from "./is.ts";
116+
*
117+
* const predObj = {
118+
* a: is.Number,
119+
* b: is.String,
120+
* c: is.Boolean,
121+
* };
122+
* const a: unknown = { a: 0, b: "a", c: true };
123+
* if (is.ObjectOf(predObj)(a)) {
124+
* // a is narrowed to { a: number, b: string, c: boolean }
125+
* const _: { a: number, b: string, c: boolean } = a;
126+
* }
127+
* ```
128+
*/
129+
export function isObjectOf<
130+
T extends RecordOf<Predicate<unknown>>,
131+
>(
132+
predObj: T,
133+
options: { strict?: boolean } = {},
134+
): Predicate<ObjectOf<T>> {
135+
return (x: unknown): x is ObjectOf<T> => {
136+
if (!isRecord(x)) {
137+
return false;
138+
}
139+
const preds = Object.entries<Predicate<unknown>>(predObj);
140+
if (options.strict) {
141+
if (Object.keys(x).length !== preds.length) {
142+
return false;
143+
}
144+
} else {
145+
if (Object.keys(x).length < preds.length) {
146+
return false;
147+
}
148+
}
149+
return preds.every(([k, p]) => p(x[k]));
150+
};
151+
}
152+
101153
/**
102154
* Return `true` if the type of `x` is `function`.
103155
*/
@@ -135,6 +187,7 @@ export default {
135187
TupleOf: isTupleOf,
136188
Record: isRecord,
137189
RecordOf: isRecordOf,
190+
ObjectOf: isObjectOf,
138191
Function: isFunction,
139192
Null: isNull,
140193
Undefined: isUndefined,

is_test.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import is, {
1010
isNull,
1111
isNullish,
1212
isNumber,
13+
isObjectOf,
1314
isRecord,
1415
isRecordOf,
1516
isString,
@@ -26,6 +27,7 @@ Deno.test("is defines aliases of functions", () => {
2627
assertStrictEquals(is.TupleOf, isTupleOf);
2728
assertStrictEquals(is.Record, isRecord);
2829
assertStrictEquals(is.RecordOf, isRecordOf);
30+
assertStrictEquals(is.ObjectOf, isObjectOf);
2931
assertStrictEquals(is.Function, isFunction);
3032
assertStrictEquals(is.Null, isNull);
3133
assertStrictEquals(is.Undefined, isUndefined);
@@ -165,6 +167,50 @@ Deno.test("isRecordOf<T>", async (t) => {
165167
});
166168
});
167169

170+
Deno.test("isObjectOf<T>", async (t) => {
171+
await t.step("returns true on T object", () => {
172+
const predObj = {
173+
a: isNumber,
174+
b: isString,
175+
c: isBoolean,
176+
};
177+
assertEquals(isObjectOf(predObj)({ a: 0, b: "a", c: true }), true);
178+
assertEquals(
179+
isObjectOf(predObj)({ a: 0, b: "a", c: true, d: "ignored" }),
180+
true,
181+
);
182+
});
183+
await t.step("returns false on non T object", () => {
184+
const predObj = {
185+
a: isNumber,
186+
b: isString,
187+
c: isBoolean,
188+
};
189+
assertEquals(isObjectOf(predObj)({ a: 0, b: "a", c: "" }), false);
190+
assertEquals(isObjectOf(predObj)({ a: 0, b: "a" }), false);
191+
assertEquals(
192+
isObjectOf(predObj, { strict: true })({
193+
a: 0,
194+
b: "a",
195+
c: true,
196+
d: "invalid",
197+
}),
198+
false,
199+
);
200+
});
201+
await t.step("returns proper type predicate", () => {
202+
const predObj = {
203+
a: isNumber,
204+
b: isString,
205+
c: isBoolean,
206+
};
207+
const a: unknown = { a: 0, b: "a", c: true };
208+
if (isObjectOf(predObj)(a)) {
209+
const _: { a: number; b: string; c: boolean } = a;
210+
}
211+
});
212+
});
213+
168214
Deno.test("isFunction", async (t) => {
169215
await t.step("returns true on function", () => {
170216
assertEquals(isFunction(isFunction), true);

0 commit comments

Comments
 (0)