Skip to content

Commit 9cdb140

Browse files
authored
Merge pull request #29 from Milly/objct-optional-property
Allow `isObjectOf` to specify an optional property
2 parents 1325620 + c6d3088 commit 9cdb140

File tree

3 files changed

+331
-77
lines changed

3 files changed

+331
-77
lines changed

is.ts

Lines changed: 55 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -146,12 +146,24 @@ type FlatType<T> = T extends RecordOf<unknown>
146146
? { [K in keyof T]: FlatType<T[K]> }
147147
: T;
148148

149-
export type ObjectOf<
150-
T extends RecordOf<Predicate<unknown>>,
151-
> = FlatType<{ [K in keyof T]: T[K] extends Predicate<infer U> ? U : never }>;
149+
type OptionalPredicateKeys<T extends RecordOf<unknown>> = {
150+
[K in keyof T]: T[K] extends OptionalPredicate<unknown> ? K : never;
151+
}[keyof T];
152+
153+
export type ObjectOf<T extends RecordOf<Predicate<unknown>>> = FlatType<
154+
& {
155+
[K in Exclude<keyof T, OptionalPredicateKeys<T>>]: T[K] extends
156+
Predicate<infer U> ? U : never;
157+
}
158+
& {
159+
[K in OptionalPredicateKeys<T>]?: T[K] extends Predicate<infer U> ? U
160+
: never;
161+
}
162+
>;
152163

153164
/**
154165
* Return a type predicate function that returns `true` if the type of `x` is `ObjectOf<T>`.
166+
* If `is.OptionalOf()` is specified in the predicate function, the property becomes optional.
155167
* When `options.strict` is `true`, the number of keys of `x` must be equal to the number of keys of `predObj`.
156168
* Otherwise, the number of keys of `x` must be greater than or equal to the number of keys of `predObj`.
157169
*
@@ -161,12 +173,12 @@ export type ObjectOf<
161173
* const predObj = {
162174
* a: is.Number,
163175
* b: is.String,
164-
* c: is.Boolean,
176+
* c: is.OptionalOf(is.Boolean),
165177
* };
166-
* const a: unknown = { a: 0, b: "a", c: true };
178+
* const a: unknown = { a: 0, b: "a" };
167179
* if (is.ObjectOf(predObj)(a)) {
168-
* // a is narrowed to { a: number, b: string, c: boolean }
169-
* const _: { a: number, b: string, c: boolean } = a;
180+
* // a is narrowed to { a: number, b: string, c?: boolean }
181+
* const _: { a: number, b: string, c?: boolean } = a;
170182
* }
171183
* ```
172184
*/
@@ -176,22 +188,16 @@ export function isObjectOf<
176188
predObj: T,
177189
options: { strict?: boolean } = {},
178190
): Predicate<ObjectOf<T>> {
179-
return (x: unknown): x is ObjectOf<T> => {
180-
if (!isRecord(x)) {
181-
return false;
182-
}
183-
const preds = Object.entries<Predicate<unknown>>(predObj);
184-
if (options.strict) {
185-
if (Object.keys(x).length !== preds.length) {
186-
return false;
187-
}
188-
} else {
189-
if (Object.keys(x).length < preds.length) {
190-
return false;
191-
}
192-
}
193-
return preds.every(([k, p]) => p(x[k]));
194-
};
191+
const preds = Object.entries(predObj);
192+
const allKeys = new Set(preds.map(([key]) => key));
193+
const requiredKeys = preds
194+
.filter(([_, pred]) => !(pred as OptionalPredicate<unknown>).optional)
195+
.map(([key]) => key);
196+
const hasKeys = options.strict
197+
? (props: string[]) => props.every((p) => allKeys.has(p))
198+
: (props: string[]) => requiredKeys.every((k) => props.includes(k));
199+
return (x: unknown): x is ObjectOf<T> =>
200+
isRecord(x) && hasKeys(Object.keys(x)) && preds.every(([k, p]) => p(x[k]));
195201
}
196202

197203
/**
@@ -273,6 +279,31 @@ export function isOneOf<T extends readonly Predicate<unknown>[]>(
273279
return (x: unknown): x is OneOf<T> => preds.some((pred) => pred(x));
274280
}
275281

282+
export type OptionalPredicate<T> = Predicate<T | undefined> & {
283+
optional: true;
284+
};
285+
286+
/**
287+
* Return a type predicate function that returns `true` if the type of `x` is `T` or `undefined`.
288+
*
289+
* ```ts
290+
* import is from "./is.ts";
291+
*
292+
* const a: unknown = "a";
293+
* if (is.OptionalOf(is.String)(a)) {
294+
* // a is narrowed to string | undefined;
295+
* const _: string | undefined = a;
296+
* }
297+
* ```
298+
*/
299+
export function isOptionalOf<T>(
300+
pred: Predicate<T>,
301+
): OptionalPredicate<T> {
302+
return Object.assign(isOneOf([isUndefined, pred]), {
303+
optional: true as const,
304+
});
305+
}
306+
276307
export default {
277308
String: isString,
278309
Number: isNumber,
@@ -292,4 +323,5 @@ export default {
292323
Nullish: isNullish,
293324
Symbol: isSymbol,
294325
OneOf: isOneOf,
326+
OptionalOf: isOptionalOf,
295327
};

is_bench.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,3 +196,13 @@ Deno.bench({
196196
}
197197
},
198198
});
199+
200+
Deno.bench({
201+
name: "is.OptionalOf",
202+
fn: () => {
203+
const pred = is.OptionalOf(is.String);
204+
for (const c of cs) {
205+
pred(c);
206+
}
207+
},
208+
});

0 commit comments

Comments
 (0)