Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Typescript - How to use nested mapped types

Tags:

typescript

I am given a large complicated interface X that I cannot modify, I would like to define some smaller types based on the properties and sub-properties within the interface X that I want to use as types for parameters of a function. I would also like to avoid redeclaring separately what is already inside X.

interface X {
  a:number;
  c:{
    aa:number;
    cc:{
      aaa:string;
    }
  },
  d?:{
    aa:number;
    cc:{
      aaa:string;
    }
  },
  e:{
    aa:number;
    cc:{
      aaa:string;
    }
  }[]
}

I understand that I can use mapped types to access the sub-type [Edit note: sub-type is the wrong terminology, this should be called Nested-level types, see answer below] this should be of the object, for example:

type C = X["c"]; // { aa:number; cc:{ aaa:string } } 

type C_CC = X["c"]["cc"]; // { aaa: string }

type D = X["d"]; // { aa:number; cc:{ aaa:string } } | undefined

However, things got a bit hairy when the property is possibly undefined. If I want to get at the type at X.d.cc...(which should give {aaa:string} ) how do I do it? Or is it even possible?

The following two attempts gives an error:

type D_CC1 = X["d"]["cc"];  // error? Due to the (d?) possibly undefined?
type D_CC2 = Required<X["d"]>["cc"];  // error also?

Separately, I am also a bit confused when arrays are involved, for example above for E:

type E = X["e"]; // { aa:number; cc:{ aaa:string } }[]
type E_CC1 = X["e"]["cc"]; // error
type E_CC2 = X["e"][0]["cc"]; // got the result I wanted: { aaa: string }

The third line seems to work, but I am not sure what is the logic of using [0] -- seems hacky. I would like to know if this is the right way.

Edit: Changed type names from D_CC to D_CC1 and D_CC2 to avoid ambiguity.

like image 717
Andrew Ooi Avatar asked Oct 03 '19 10:10

Andrew Ooi


2 Answers

Just a detail but in order to not mix up terminology, what you have in your interface are not subtypes, but nested-level types. Nested-level types have top-level types. Subtypes are types that you can use wherever a type that has this subtype is required.

Next, what you are calling mapped types are actually what we call the keying-in operator. That's a way to look up property types in a shape.

Mapped types are used to map over an object's key and value types and it looks like this:

type MyMappedType = {
  [Key in K]: ValueType
}

That's also where you could leverage the keyof operator. Key in keyof Something

For example, to make all fields in a shape optional (there's a built-in mapped types called Partial<Object> for this), you could use mapped types:

type ShapeAllOptional = {
  [K in keyof MyShape]?: MyShape[K]
}

Now on to your question.

type D_CC1 = X["d"]["cc"];  // error? Due to the (d?) possibly undefined?

You could use something like this

type myStype = NonNullable<X['d']>['cc']

As to using

type E_CC1 = X["e"]["cc"]; // error
type E_CC2 = X["e"][0]["cc"]; // got the result I wanted: { aaa: string }

Yes, it's an array with each item having that shape. I don't know if it's hacky but I did see something once that looks a bit "cleaner".

type E_CC2 = X["e"][number]["cc"];

There's no better way I'm aware of.

Ah, I almost forgot.

type D_CC2 = Required<X["d"]>["cc"];  // error also?

Careful, what you're saying here is make all properties in X['d'] required, that is aa and cc, which are... already required. Required<X> would I think achieve what you want.

like image 192
jperl Avatar answered Nov 17 '22 09:11

jperl


If you take a look at your interface, you'll notice that e property is actually defined as array.

e:{
    aa:number;
    cc:{
      aaa:string;
    }
}[] // NOTICE THIS!

That's the reason you need to use indexer [0] to get the type of item of that array. It's not a hack, actually it's added in typescript since 2.1 and the official name is indexed access types, also called lookup types.

like image 26
zhuber Avatar answered Nov 17 '22 09:11

zhuber