Home > Blockchain >  Typescript: accessing interface subtypes with square brackets... problem with optional properties
Typescript: accessing interface subtypes with square brackets... problem with optional properties

Time:03-29

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

TypeScript Playground Link

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).

TypeScript playground

  • Related