'TypeScript: Generic extends object with only optional keys
I have a function with overload
export function someFunc<P extends undefined>(): () => string
export function someFunc<P extends object>(): (params: P) => string
export function someFunc<P extends object | undefined>() {
// ...implementation
}
The thing I'd like to achieve is to be able to add one more overload for the case where P
doesn't have any required keys, eg
const obj: P = { foo?: string, bar?: string } // matches
const obj2: P = { foo: string, bar?: string } // doesn't match
the overload would look something like this:
export function someFunc<P extends NoOptionalKeys>(): (params?: P) => string
Solution 1:[1]
Hmm, I don't know if I've ever seen overloads used in this way; I suppose you expect the caller to manually specify the P
generic, and then have the compiler pick the call signature from that, since the call signatures are all zero-argument and there's nothing from which to infer P
automatically. So I'm assuming you're looking for something like this:
const fUndef = someFunc<undefined>();
// () => string
const fSomeReq = someFunc<{ a: string, b?: string }>();
// (params: {a: string, b?: string}) => string;
const fNoReq = someFunc<{ a?: string, b?: string }>();
// (params?: {a?: string, b?: string}) => string;
Doing this with overloads is a bit strange because generic constraints are upper bounds. P extends XXX
means that P
must be some subtype of XXX
(so it should be XXX
, or something narrower than XXX
).
But in TypeScript, an object with all optional properties is a supertype of the same object with any required properties, not a subtype. So the thing you're looking for is essentially a lower bound generic constraint, but TypeScript doesn't directly support that. (See microsoft/TypeScript#14520 for more information.) You'd want to say something like P super Partial<P>
instead of P extends Partial<P>
. Or, equivalently, Partial<P> extends P
, but generic constraints don't work that way.
You can simulate lower bound constraints using conditional types, so instead of P extends UpperBound super LowerBound
, you can write P extends (LowerBound extends P ? UpperBound : never)
and it has a chance of working. For you, that means this:
function someFunc<P extends undefined>(): () => string;
function someFunc<P extends (Partial<P> extends P ? object : never)>(): (params?: P) => string
function someFunc<P extends object>(): (params: P) => string;
function someFunc() {
return null!
}
I put this as the second overload; it needs to come before the regular object
overload so that it has higher priority. If you check this code, the above fUndef
, fSomeReq
, and fNoReq
are typed the way you expect.
Another possible way to proceed is to give only a single call signature and use conditional types to calculate the function output type. Something like this:
function someFunc<P extends object | undefined>(): (...params:
P extends undefined ? [] :
Partial<P> extends P ? [P?] :
[P]
) => string;
function someFunc() {
return null!
}
This should behave very similarly (and you'll see that fUndef
, fSomeReq
, and fNoReq
are the right types), but allows us to write our logic a little more straightforwardly: Partial<P> extends P
instead of the reverse, because P
is already determined by the caller and instead of verifying a constraint, we're testing it with conditional types.
Notice that the return type above is a single callable with a tuple-typed rest parameter that's conditionally determined. I did that because all three output types are callables returning a string
. You could write it out like this instead:
function someFunc<P extends object | undefined>():
P extends undefined ? () => string :
Partial<P> extends P ? (params?: P) => string :
(params: P) => string
function someFunc() {
return null!
}
which might be more obviously related to your overloads. Either way should work.
Okay, hope that helps; good luck!
Solution 2:[2]
I know this is a bit old, but depending on what exactly you want, you could choose either:
- Only required fields.
- Only optional fields.
- Cast all fields to be optional.
where each of those options could be shallow (only top-level keys) or deep (every nested key of the previous level's keys).
I've done this before using these util types:
/*************************************
*** Sub-utils for ease of (re)use ***
*** Optional, makes code more DRY ***
*************************************/
/**
* Same as Nullable except without `null`.
*/
export type Optional<T> = T | undefined;
/**
* Opposite of built-in `NonNullable`.
*/
export type Nullable<T> = Optional<T> | null;
/**
* Types that can be used to index native JavaScript types, (Object, Array, etc.).
*/
export type IndexSignature = string | number | symbol;
/**
* An object of any index-able type to avoid conflicts between `{}`, `Record`, `object`, etc.
*/
export type Obj<O extends Record<IndexSignature, unknown> | object = Record<IndexSignature, unknown> | object> = {
[K in keyof O as K extends never
? never
: K
]: K extends never
? never
: O[K] extends never
? never
: O[K];
} & Omit<O, never>;
/**
* Any type that is indexable using `string`, `number`, or `symbol`.
*
* Serves as a companion to {@link OwnKeys} while maintaining the generalizable usage of {@link Obj}.
*/
export type Indexable<ValueTypes = unknown> = (
{
[K: IndexSignature]: ValueTypes;
}
| Obj
);
/********************************************
*** Actual typedefs to achieve your goal ***
********************************************/
/**
* Picks only the optional properties from a type, removing the required ones.
* Optionally, recurses through nested objects if `DEEP` is true.
*/
export type PickOptional<T, DEEP extends boolean = false> = { // `DEEP` must be false b/c `never` interferes with root level objects with both optional/required properties
// If `undefined` extends the type of the value, it's optional (e.g. `undefined extends string | undefined`)
[K in keyof T as undefined extends T[K]
? K
: never
]: DEEP extends false
? T[K]
: T[K] extends Optional<Indexable> // Like above, we must include `undefined` so we can recurse through both nested keys in `{ myKey?: { optionalKey?: object, requiredKey: object }}`
? PickOptional<T[K], DEEP>
: T[K];
};
/**
* Picks only the required fields out of a type, removing the optional ones.
* Optionally, recurses through nested objects if `DEEP` is true.
*/
export type PickRequired<T, DEEP extends boolean = false> = {
[K in keyof T as K extends keyof PickOptional<T, DEEP>
? never
: K
]: T[K] extends Indexable
? PickRequired<T[K], DEEP>
: T[K];
};
/**
* Companion to built-in `Partial` except that it makes each nested property optional
* as well.
*
* Each non-object key's value will be either:
* - If `NT` (NewType) is left out, then the original type from `T` remains.
* - The specified `NT` type.
*/
export type PartialDeep<T, NT = never> = T extends Indexable
? {
// `?:` forces the key to be optional
[K in keyof T]?: T[K] extends Indexable
? Nullable<PartialDeep<T[K], NT>>
: NT extends never
? Nullable<T[K]>
: Nullable<NT>
}
: NT extends never
? Nullable<T>
: Nullable<NT>;
For your case, you could use them like:
// Includes both `P extends undefined` and `P extends object`
export function someFunc<P extends Optional<Indexable>>(params: P): string;
// Only includes optional keys from `P extends object`
export function someFunc<P extends PickOptional<Indexable>>(params: P): string;
// What I think you're seeking which is that every key is optional
export function someFunc<P extends PartialDeep<Indexable>>(params: P): string;
// Actual implementation needs to allow for each of:
// - undefined
// - object
// - PickOptional<object>
// - PartialDeep<object>`
export function someFunc<P extends Optional<PartialDeep<Indexable>>>(params: P) {
return params ? JSON.stringify(params) : 'No params';
}
While I added an incomplete implementation, it at least shows how you could use the overloads and type utils.
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | jcalz |
Solution 2 | yuyu5 |