@@ -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+
276307export 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} ;
0 commit comments