typescript - Bagaimana cara membuat satu tipe objek yang akan berisi properti dari semua objek tipe union, tetapi nilainya adalah irisan


Bagaimana saya bisa membuat sebuah tipe yang akan membuat satu tipe objek dari gabungan objek-objek, yang akan berisi properti dari semua objek dari tipe gabungan, tetapi nilainya akan menjadi irisan?

Contoh: Saya perlu membuat dari tipe { foo: 1 } | { foo: 2; bar: 3 } | { foo: 7; bar: 8 } sebuah tipe {foo: 1 | 2 | 7; bar: 3 | 8}.

Catatan penting: Saya ingin membuat satu tipe objek daripada sebuah irisan seperti {foo: 1 | 2} & {bar: 3}

Saya telah menulis sebuah tipe, ComplexUnionToIntersection, yang seharusnya melakukannya, tetapi mengabaikan properti yang tidak ada di semua objek dalam union (bar dalam contoh saya).

Kode saya:

/**
 * More info: https://fettblog.eu/typescript-union-to-intersection/
 */
export type UnionToIntersection<U> = (
    U extends any ? (k: U) => void : never
) extends (k: infer I) => void
    ? I
    : never;

/**
 * Target type
 */
export type ComplexUnionToIntersection<U> = { o: U } extends { o: infer X }
    ? {
            [K in keyof (X & U)]: (X & U)[K];
      }
    : UnionToIntersection<U>;

Kasus uji:

// TODO: result of test case must be `{foo: 1 | 2; bar: 3}`
type testCase1 = ComplexUnionToIntersection<{ foo: 1 } | { foo: 2; bar: 3 }>; // actually return `{ foo: 1 | 2; }`

// TODO: result of test case must be `{foo: 1 | 2 | 7; bar: 3 | 8}`
type testCase2 = ComplexUnionToIntersection<
    { foo: 1 } | { foo: 2; bar: 3 } | { foo: 7; bar: 8 }
>;

// TODO: result of test case must be `{foo: 1 | 2; bar: 3 | 8}`
type testCase3 = ComplexUnionToIntersection<
    { foo: 1 } | { foo: 2; bar: 3 } | { bar: 8 }
>;

// TODO: result of test case must be `{foo?: 1 | 2; bar: 3 | 8}`
type testCase4 = ComplexUnionToIntersection<
    { foo: 1 } | { foo?: 2; bar: 3 } | { bar: 8 }
>;

TS Playground ada di sini

Jawaban #1

Jadi operasi ini adalah untuk menggabungkan sekumpulan tipe objek menjadi satu tipe objek tunggal, di mana setiap kunci properti dalam setiap anggota union input akan muncul di tipe keluaran, dan tipe nilai dari properti itu akan menjadi union dari semua tipe nilai properti untuk kunci itu di mana pun ia muncul dalam input. Dan Anda ingin sebuah properti menjadi opsional dalam keluaran jika ia opsional dalam setiap anggota union input.

Jika Anda hanya menggabungkan union menjadi satu jenis objek, Anda akan menghadapi masalah yang terlihat di mana suatu properti hanya akan ada di output jika ada di setiap input, tetapi sebaliknya jenis properti akan benar. Jadi salah satu pendekatan di sini adalah dengan terlebih dahulu menambah setiap anggota union sehingga mengandung semua kunci dari anggota union mana pun. Jadi jika suatu properti hilang, kita harus menambahkannya, dan memberi jenis nilai never, yang diserap di dalam union apa pun.

Sebagai contoh, kita mulai dengan sesuatu seperti:

{ foo: 1 } | { foo?: 2; bar: 3 } | { bar: 8 }

Dan kemudian tingkatkan setiap anggota dari union agar berisi semua kunci, sesuatu seperti:

{ foo: 1; bar: never } | { foo?: 2; bar: 3 } | { foo: never; bar: 8 }

Dan kemudian kita menggabungkan serikat baru ini menjadi satu objek, seperti:

{ foo?: 1 | 2 | never; bar: never | 3 | 8 }

yang runtuh menjadi

{ foo?: 1 | 2; bar: 3 | 8 }

Jadi mari kita lakukan:


type AllKeys<T> = T extends unknown ? keyof T : never

type AddMissingProps<T, K extends PropertyKey = AllKeys<T>> =
    T extends unknown ? (T & Record<Exclude<K, keyof T>, never>) : never;

type MyMerge<T> = { [K in keyof AddMissingProps<T>]: AddMissingProps<T>[K] }

Tipe AllKeys<T> adalah tipe kondisional distributif yang mengumpulkan setiap kunci dari setiap anggota union:

type TestAllKeys = AllKeys<{ foo: 1 } | { foo?: 2; bar: 3 } | { bar: 8 }>
// type TestAllKeys = "foo" | "bar"

Tipe AddMissingProps<T, K> juga distributif, dan untuk setiap elemen dari union input T ia menambahkan kunci dari K yang belum ada di keyof T, dan memberinya tipe never, serta perhatikan bahwa K default-nya adalah AllKeys<T>:

type TestAddMissingProps = AddMissingProps<{ foo: 1 } | { foo?: 2; bar: 3 } | { bar: 8 }>
/* type TestAddMissingProps = 
    ({ foo: 1; } & Record<"bar", never>) | 
    ({ foo?: 2 | undefined; bar: 3; } & Record<never, never>) | 
    ({ bar: 8; } & Record<"foo", never>) */

Itu setara dengan tipe { foo: 1; bar: never } | { foo?: 2; bar: 3 } | { foo: never; bar: 8 } yang disebutkan di atas, meskipun tidak ditulis dengan cara yang sama. Karena kita masih akan memproses tipe tersebut, tidak penting untuk menyederhanakannya di sini.

Akhirnya, tipe MyMerge<T> adalah tipe terpeta identity atas AddMissingProps<T>. Tujuan satu-satunya adalah untuk menelusuri setiap properti dan menghasilkan output tipe objek tunggal:

type TestMyMerge = MyMerge<{ foo: 1 } | { foo?: 2; bar: 3 } | { bar: 8 }>
/* type TestMyMerge = {
    foo?: 1 | 2 | undefined;
    bar: 3 | 8;
} */

Terlihat bagus!

Tautan playground ke kode

Artikel terkait