0

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;
}
Ayyappa
  • 1,876
  • 1
  • 21
  • 41

2 Answers2

2

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.

aleksxor
  • 7,535
  • 1
  • 22
  • 27
  • I see a problem. Here address should only be undefined if it has optional/no parameters only. – Ayyappa Jul 05 '21 at 05:36
  • aleksxor = GOD SENT! I will go through it in detail but a quick check seems to work for my case! Thanks a lot! – Ayyappa Jul 05 '21 at 07:40
  • @aleksxor this https://stackoverflow.com/questions/68205975/how-can-i-have-one-explicit-and-one-inferred-type-parameter/68206980#68206980 question might be interesting for you. Maybe you will find some better approach – captain-yossarian from Ukraine Jul 05 '21 at 07:46
  • What happens if this (UndefinedIfHasNoRequired) resolves to never in Undefine | UndefinedIfHasNoRequired ? Ex: Undefine | never =====> Undefined ? – Ayyappa Jul 05 '21 at 07:48
  • 1
    You may think of `|` as `+` (plus) on the type level. And `never` is kind of `0` (zero) on type leverl. So _any_ type plus `never` is the same type: `string | never` ~= `string` for example. – aleksxor Jul 05 '21 at 07:50
  • Sweet! Highlight part in the solution is usage of HasNoKeys to achieve equality operation. I was banging my head to compare T[K] with {[index:string] : never} (which is eventually meaning no keys) – Ayyappa Jul 05 '21 at 07:55
  • 1
    Well, I believe the part `keyof T extends never` is pretty obvious. Only `T` object with no keys is an empty object. Otherwise `keyof T` will be a union of it's keys and something cannot extend nothing (`never`). – aleksxor Jul 05 '21 at 07:59
  • ``` type Undefine = { [K in keyof T]: T[K] extends object ? HasNoKeys extends 1 ? undefined : Undefine : T[K] | undefined } ``` Any reason why this isn't working? I removed UndefinedIfHasNoRequired and inlined it to have another conditional. – Ayyappa Jul 05 '21 at 08:14
  • First, you've omitted `RequiredOnly` check and second they're not quite _equal_. Since it's pretty much to explain in a comment I made a playground https://tsplay.dev/Nal6ow – aleksxor Jul 05 '21 at 08:33
  • One last question. How can i exclude array types here and only consider maps alone? – Ayyappa Jul 05 '21 at 09:18
  • Include a check for array type. Something like `T extends any[]` – aleksxor Jul 05 '21 at 09:26
0

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

Chirag Shah
  • 464
  • 5
  • 9
  • Actually i don't want to declare undefined for address. I'm looking for an option where if its possible to have a conditional type to evaluate as undefined if no properties exist (here in this case address has no required properties - all are optional) – Ayyappa Jul 05 '21 at 06:50
  • When no properties exist and you define an address like `address:{}` it will be considered as an object because an object can have no properties. Typescript will convert your code into javascript and in javascript blank object is possible which is not undefined. So you need to define in types as undefined if it's possible. – Chirag Shah Jul 05 '21 at 06:55
  • How to identify address:{} with conditional types in typescript? – Ayyappa Jul 05 '21 at 07:09
  • Hey, In the example please use street and pincode as optional parameters – Ayyappa Jul 05 '21 at 08:52