Urmzd
Urmzd

Reputation: 713

TypeScript - Unable to access recursive keys

I have implemented a type which takes an object and returns a type without any nullish values.

export type OmitNullish<T> = Exclude<T, null | undefined>;
export type OmitNullishKeys<T> = {
    [K in keyof T]-?: T[K] extends boolean | string | number | symbol ? OmitNullish<T[K]> : OmitNullishKeys<T[K]>;
};

However, when I attempt to retrieve a nested key, tsc using:

export type RandomObj = OmitNullishKeys<{
    stackOverflow: {
        forums?: {
            thread1: 'not available';
        } | null;
    } | null;
}>;
export type RandomObjectAccessed = RandomObj['stackOverflow']['forums'];

it states the following:

Property 'forums' does not exist on type 'OmitNullishKeys<{ forums?: { thread1: "not available"; } | null | undefined; } | null>'.

It seems as the resulting type is treated as OmitNullishKeys instead of an object without nullish values. Is there a reason for this?

Upvotes: 0

Views: 117

Answers (2)

Urmzd
Urmzd

Reputation: 713

The issue was resolved initially by:

export type N = null | undefined;
export type OmitNullish<T> = T extends (infer U)[] ? Exclude<U, N>[] : Exclude<U,N>
export type OmitNullishKeys<T> = {
  [key in keyof Required<T>]-?: T extends Record<string, unknown> ? OmitNullish<OmitNullishKeys<T[K]>> : OmitNullish<T[K]>
}

Which resolved the issue

  1. The exclusion of nullish values were not applied to objects due to the lack of OmitNullish
  2. When keyof is used without enforcing non-optionality, the resulting value is T | undefined. Required forces the key to be treated as it were there.

but applied the non-nullish rules to classes such as Date.

Using the alternate solution provided by @jcalz with the addition of a conditional to avoid overwriting classes, we get

type OmitNullish<T, E = Date> = T extends E ? T : NonNullable<{ 
  [key in keyof T]-?: OmitNullish<T[K]>
}>

which gives us a succinct way of ensuring all elements within an object are not-nullish.

Upvotes: 0

Olian04
Olian04

Reputation: 6872

If we introduce a helper type for expanding the type of RandomObj. Then we can clearly see that your logic in OmitNullishKeys is faulty. The resulting type RandomObj does still contain values that might be null.

type ExpandType<T> = T extends object
  ? T extends infer O ? { [K in keyof O]: ExpandType<O[K]> } : never
  : T;

type ExpandedRandomObj = ExpandType<RandomObj>;
/*
type ExpandedRandomObj = {
    stackOverflow: {
        forums: {
            thread1: 'not available';
        } | null;
    } | null;
}
*/

TS playground

Upvotes: 2

Related Questions