interface Body {
size: number;
meta?: Meta;
}
export interface Request {
body?: Body;
}
I thought that this will work:
type MyMeta = Request["body"]["meta"];
However, because body
is optional, type of Request["body"]
is Body | undefined
, so I'm getting
Property 'meta' does not exist on type 'Body | undefined'.ts(2339)
Is there any other way than things like:
type MyMeta = Exclude<Request["body"], undefined>["meta"];
type MyMeta = Required<Request>['body']['meta'];
?
Those will be problematic for deeper properties.
Using DeepRequired
generic is also bad idea, now every property in meta
is required:
export type DeepRequired<T> = Required<{
[K in keyof T]: DeepRequired<T[K]>
}>
type MyMeta = DeepRequired<Request>['body']['meta'];
CodePudding user response:
You can use the built-in NonNullable
type which is just an alias for Exclude<T, undefined | null>
:
interface Body {
size: number;
meta?: { __notSpecified: never };
}
export interface Request {
body?: Body;
}
type MyMeta = NonNullable<Request["body"]>["meta"]; // { __notSpecified: never; } | undefined
Other than that, though, there's really nothing else you can do.
CodePudding user response:
You can create a recursive type Index
that takes an array of keys and traverses the object type while discarding undefined
alternatives:
type Index<P, T> =
P extends readonly [infer Key, ...infer Rest]
? T extends undefined ? never : Index<Rest, T[Extract<Key, keyof T>]>
: T
Because Index
doesn't constrain the path type, it will simply yield never
for incorrect paths. This can be remedied by declaring a Shape
type that builds and object with optional keys for each of the path keys (stolen from my answer to another question https://stackoverflow.com/a/67351133/5770132, see that answer for more details).
type Shape<P> =
P extends readonly [infer Key, ...infer Rest]
? {[K in Extract<Key, PropertyKey>]?: Shape<Rest>}
: unknown
Using Shape
, we can declare a DeepIndex
that gives an error on incorrect paths:
type DeepIndex<T extends Shape<Path>, Path extends readonly PropertyKey[]> = Index<Path, T>
A few examples (with an added definition for Meta
):
interface Meta { x: 42, meta: Meta }
interface Body { size: number; meta?: Meta }
export interface Request { body?: Body }
{ type Test = DeepIndex<Request, ['body']> }
// type Test = Body | undefined
{ type Test = DeepIndex<Request, ['body', 'size']> }
// type Test = number
{ type Test = DeepIndex<Request, ['body', 'meta', 'x']> }
// type Test = 42
{ type Test = DeepIndex<Request, ['body', 'meta', 'meta', 'meta', 'x']> }
// type Test = 42
{ type Test = DeepIndex<Request, ['nobody', 'size']> }
// ERROR: Type 'Request' has no properties in common with type
// '{ nobody?: { size?: unknown; } | undefined; }
Note that the result includes undefined
if the last property is optional, but won't automatically do so if any of the preceding properties are optional. Propagating optionality down the path would be possible but complicates things quite a bit. Removing undefined
from the result is also possible, but that would return never
for properties that happen to have the type undefined
(which are probably not that common though).