Typescript: Combining types in depth

A step-by-step tutorial on how to write a generic type in TypeScript that combines arbitrary nested key-value structures.

Translator's note: I deliberately did not translate some words (like generic, key-value), because, in my opinion, this will only complicate the understanding of the material.

TLDR:

The source code for DeepMergeTwoTypeswill be at the end of the article. Copy it to your IDE to play with it.

How it looks in vsCode:

, generic- TypeScript, (Miniminalist Typescript - Generics)

IDE (. : TypeScript Playground ).

Disclaimer

production ( , ).

&- Typescript

. A B C, A & B

type A = { key1: string, key2: string }
type B = { key2: string, key3: string }
type C = A & B
const a = (c: C) => c.

, .

type A = { key1: string, key2: string }
type B = { key2: null, key3: string }
type C = A & B

A key2 , B null.

Typescript never C . - :

type ExpectedType = {
  key1: string | null,
  key2: string,
  key3: string
}

generic-, Typescript. 2 generic-.

GetObjDifferentKeys<>

type GetObjDifferentKeys<T, U> = Omit<T, keyof U> & Omit<U, keyof T>

2 , A B.

type A = { key1: string, key2: string }
type B = { key2: null, key3: string }

type C = GetObjDifferentKeys<A, B>['']

GetObjSameKeys<>

generic- , , .

type GetObjSameKeys<T, U> = Omit<T | U, keyof GetObjDifferentKeys<T, U>>

β€” .

type A = { key1: string, key2: string }
type B = { key2: null, key3: string }
type C = GetObjSameKeys<A, B>

, generic- DeepMergeTwoTypes

DeepMergeTwoTypes<>

type DeepMergeTwoTypes<T, U> =
  // " " ()  - 
  Partial<GetObjDifferentKeys<T, U>>
  //   - 
  & { [K in keyof GetObjSameKeys<T, U>]: T[K] | U[K] }

generic " " T U, (). Partial<>, Typescript. ( &-) T U , T[K] | U[K].

. generic "-" (?), .

type A = { key1: string, key2: string }
type B = { key2: null, key3: string }
const fn = (c: DeepMergeTwoTypes<A, B>) => c.

 DeepMergeTwoTypes generic . generic  MergeTwoObjects   DeepMergeTwoTypes  , .

//  generic   DeepMergeTwoTypes<>
type MergeTwoObjects<T, U> =
  // " " ()  - 
  Partial<GetObjDifferentKeys<T, U>>
  //   - 
  & {[K in keyof GetObjSameKeys<T, U>]: DeepMergeTwoTypes<T[K], U[K]>}

export type DeepMergeTwoTypes<T, U> =
  //     ,    
  [T, U] extends [{ [key: string]: unknown }, { [key: string]: unknown } ]
    ? MergeTwoObjects<T, U>
    : T | U

PRO TIP: , DeepMergeTwoTypes if-else (extends ?:) T U , (tuple) [T, U]. &&- Javascript.

generic ,  { [key: string]: unknown } ( Object). ,  MergeTwoObject<>. .

: extends { [key: string]: unknown } -, .. , , booleans ...

! generic . :

type A = { key: { a: null, c: string} }
type B = { key: { a: string, b: string} }
const fn = (c: MergeTwoObjects<A, B>) => c.key.

?

, . generic .

, , infer (to infer - ).

infer  ( ).  infer  (Type inference in conditional types).

infer. (Item):

export type ArrayElement<A> = A extends (infer T)[] ? T : never

// Item === (number | string)
type Item = ArrayElement<(number | string)[]>

, , .  DeepMergeTwoTypes  .

export type DeepMergeTwoTypes<T, U> =
  // ----- 2   ------
  //  ⏬
  [T, U] extends [(infer TItem)[], (infer UItem)[]]
    // ...   ⏬
    ? DeepMergeTwoTypes<TItem, UItem>[]
    : ... rest of previous generic ...

DeepMergeTwoTypes , .

type A = [{ key1: string, key2: string }]
type B = [{ key2: null, key3: string }]
const fn = (c: DeepMergeTwoTypes<A, B>) => c[0].

! ?

... . Nullable non-nullable.

type A = { key1: string }
type B = { key1: undefined }

type C = DeepMergeTwoTypes<A, B>['key']

β€” string | undefined, . if-else .

export type DeepMergeTwoTypes<T, U> =
  [T, U] extends [(infer TItem)[], (infer UItem)[]]
    ? DeepMergeTwoTypes<TItem, UItem>[]
    : [T, U] extends [{ [key: string]: unknown}, { [key: string]: unknown } ]
      ? MergeTwoObjects<T, U>
      // ----- 2   ------
      //  ⏬
      : [T, U] extends [
          { [key: string]: unknown } | undefined, 
          { [key: string]: unknown } | undefined 
        ]
        // ...   ⏬
        ? MergeTwoObjects<NonNullable<T>, NonNullable<U>> | undefined
          : T | U

nullable :

type A = { key1: string }
type B = { key1: undefined }


const fn = (c: DeepMergeTwoTypes<A, B>) => c.key1;

... !

! nullable , .

generic :

type A = { key1: { a: { b: 'c'} }, key2: undefined }
type B = { key1: { a: {} }, key3: string }


const fn = (c: DeepMergeTwoTypes<A, B>) => c.

:

/**
 *  2  T  U    ,   
 * .   `DeepMergeTwoTypes`
 */
type GetObjDifferentKeys<T, U> = Omit<T, keyof U> & Omit<U, keyof T>
/**
 *  2  T and U       
 *   `DeepMergeTwoTypes`
 */
type GetObjSameKeys<T, U> = Omit<T | U, keyof GetObjDifferentKeys<T, U>>
type MergeTwoObjects<T, U> =
  // " "  
  Partial<GetObjDifferentKeys<T, U>>
  //       `DeepMergeTwoTypes<...>`
  & { [K in keyof GetObjSameKeys<T, U>]: DeepMergeTwoTypes<T[K], U[K]> }

//  2 
export type DeepMergeTwoTypes<T, U> =
  //     ,    
  //  
  [T, U] extends [(infer TItem)[], (infer UItem)[]]
    ? DeepMergeTwoTypes<TItem, UItem>[]
    //    
    : [T, U] extends [
         { [key: string]: unknown}, 
         { [key: string]: unknown } 
      ]
      ? MergeTwoObjects<T, U>
      : [T, U] extends [
          { [key: string]: unknown } | undefined, 
          { [key: string]: unknown } | undefined 
        ]
        ? MergeTwoObjects<NonNullable<T>, NonNullable<U>> | undefined
          : T | U

// :
type A = { key1: { a: { b: 'c'} }, key2: undefined }
type B = { key1: { a: {} }, key3: string }

const fn = (c: DeepMergeTwoTypes<A, B>) => c.key

How can I fix the  DeepMergeTwoTypes<T, U> generic so that it can take  N arguments instead of two?

I'll leave this stuff for the next article, but you can see my working draft here ).

Translator's note

This is my first translation experience. You are kindly requested to write in a personal message about typos, commas and simply tongue-tied phrases.




All Articles