Ayyappa
Ayyappa

Reputation: 1984

Define nested object with "optional" parameters leaf key values as undefined in typescript

This is a follow up question to this. Here the object can have optional parameters and the undefinedAllLeadNodes will like below

Input:
class Person {
    name: string = 'name';
    address: {street?: string, pincode?: string} = {};
}

const undefperson = undefinedAllLeadNodes(new Person);
console.log(undefperson);

Output:
Person: {
  "name": undefined,
  "address": undefined
} 

As you can see as address has no properties, it should return as undefined.

How can I make sure Undefine(defined here) type handles this? Currently it accepts undefperson.address.street = '';

But I want to let it throw an error with "address may be undefined"

Update:

export function undefineAllLeafProperties<T extends object>(obj : T) {

    const keys : Array<keyof T> = Object.keys(obj) as Array<keyof T>;

    if(keys.length === 0) 
        return undefined!;//This makes sure address is set to undefined. Now how to identify this with typescript conditionals so that when accessing undefperson.address.street it should say address may be undefined.

    keys.forEach(key => {
    
        if (obj[key] && typeof obj[key] === "object" && !Array.isArray(obj[key])) {
            obj[key] = undefineAllLeafProperties(<any>obj[key]) as T[keyof T];
        } else if(obj[key] && Array.isArray(obj[key])) {
            obj[key] = undefined!;
        } else {
            obj[key] = undefined!;
        }
    });

    return obj;
}

Upvotes: 0

Views: 1079

Answers (2)

Chirag Shah
Chirag Shah

Reputation: 474

You can update your class Person to following and it should work.

class Person {
    name: string = 'name';
    address: { street?: string, pincode?: string } | undefined = { street: 'street', pincode: '44555' };
}

Find the Playground Link

Update

You can do it differently like below as well

type Undefine<T extends object> = Id<{
    [K in keyof T]: T[K] extends object ? Undefine<T[K]> | undefined : T[K] | undefined
}>

Find the Playground Link

Note:

type Person {
   name?: string
   address?: {
      street?: string
      pincode?: string
   } 
}

is equivalent to

type Person {
   name: string | undefined
   address: {
      street: string | undefined
      pincode: string | undefined
   } | undefined
}

In typescript ? is used for denoting optional param which is basically undefined

Upvotes: 0

aleksxor
aleksxor

Reputation: 8340

First we'll need a couple of helper types:

type HasNoKeys<T extends object> = keyof T extends never ? 1 : 0

type RequiredOnly<T> = {
    [K in keyof T as T[K] extends Required<T>[K] ? K : never]: T[K]
}

The first one check whether the passed T object has no keys. The second one is a bit more complex. We're using remappine kyes in mapped types feature to remove optional fields.

And finally combile those type to result in undefined only when T object has no required fields (or no fields at all, because object having no fields have no required fields aswell):

type UndefinedIfHasNoRequired<T> = 
    HasNoKeys<RequiredOnly<T>> extends 1 ? undefined : never

And the final type will look like:

type Undefine<T extends object> = {
    [K in keyof T]: T[K] extends object 
        ? Undefine<T[K]> | UndefinedIfHasNoRequired<T[K]> 
        : T[K] | undefined
}

playground link

Here we're adding | undefined to the type of the object field only if it has no required fields.

Though now you'll have to assure typescript that your inner properties are not undefined when trying to assign values to their fields:

class OptionalPerson {
    name: string = 'name';
    address: {street?: string, pincode?: string} = {street: 'street', pincode: '44555'};
}

const undefOptionalPerson = undefineAllLeafProperties(new OptionalPerson())

undefOptionalPerson.address.street = '' // error

undefOptionalPerson.address!.street = ''
// or
if (undefOptionalPerson.address) {
    undefPerson.address.street = ''
}

In the first case we're using non-null assertion operator to make typescript believe our object's address field is not undefined. Though keep in mind that if in fact it's still undefined you'll get runtime error here.

In the second case we're using legit type narrowing to actually check whether the field has truthy value. And accounting for it's type object | undefined this check discards the | undefined part.

Upvotes: 2

Related Questions