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 DeepMergeTwoTypes
will 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.