Fungsi TypeScript menerima pasangan argumen variadik


Apakah mungkin di TypeScript untuk mengetik tanda tangan fungsi sehingga fungsi menerima daftar datar dari argumen tuple yang disebarkan? Wah, ini sulit dijelaskan dengan kata-kata. Contoh:

type Pair = { type: string, value: number };

function makePairs(
  t1: string, v1: number,
  t2: string, v2: number,
  ...and_so_on: unknown
): Pair[] {
  // ...
}

makePairs('a', 1, 'b', 2, 'c', 3);
// => [
//   { type: 'a', value: 1 },
//   { type: 'b', value: 2 },
//   { type: 'c', value: 3 },
// ]

Saya tidak tertarik pada implementasi isi fungsi, hanya untuk memperjelas - hanya pada mengubah sebuah tuple seperti [A, B] menjadi tuple variadik seperti [A, B, A, B, A, B, ...].

Langkah kedua dari masalah ini adalah membuatnya bekerja dengan definisi Pair yang sedikit lebih rumit, misalnya:

type StrPair = { type: 's', value: string };
type IntPair = { type: 'i', value: number };
type BoolPair = { type: 'b', value: boolean };
type Pair = StrPair | IntPair | BoolPair;

type PairType = Pair['type'];
// => 's' | 'i' | 'b'

type PairOfType<T extends PairType> =
  | (T extends 's' ? StrPair : never)
  | (T extends 'i' ? IntPair : never)
  | (T extends 'b' ? BoolPair : never);
// => PairOfType<'s'> === StrPair

type ValueOfType<T extends PairType> = PairOfType<T>['value'];
// => ValueOfType<'s'> === string

type PairTuple<T extends PairType> = [T, ValueOfType<T>];
// => ['i', 123] ✓
//    ['s', 'foo'] ✓
//    ['b', 'nope'] ⨉
// etc

// Now I'd like to be able to take something (conceptually) like this:
function makePairs(...t1: PairTuple, ...t2: PairTuple, ...moreTuples: unknown) {
  // ...
}

// And generalise to something like this:
function makePairs(...pairs: ...[...PairTuple[]]) {
  // ...
}

// ... but obviously neither of those syntaxes work

Pada dasarnya, saya ingin menghindari harus membungkus setiap pasangan dalam sebuah array saat memanggil fungsi, karena itu tampak seperti beban yang tidak perlu (percayalah, di tempat ini akan digunakan, bebannya akan signifikan).

Ada ide?

Jawaban #1

Persiapan

Perlakukan Argumen sebagai Tuple Besar

Akan sangat membantu untuk memikirkan kumpulan argumen fungsi sebagai n-tuple yang besar. Misalnya, dalam makePairs("s", "string", "b", true, "i", 42), anggap argumen-argumen tersebut sebagai tuple ["s", "string", "b", true, "i", 42]

Mulai Dengan Tipe Generik T

Karena ada kemungkinan kombinasi tuple variadik tak terbatas yang dapat dirakit untuk fungsi Anda, satu-satunya cara untuk mendekati solusi adalah melalui tipe generik. Tipe generik memungkinkan kita menggunakan tipe kondisional rekursif untuk membangun tipe kompleks dan dinamis dari beberapa prinsip dasar.

Jadi, untuk memulai, buatlah tipe generik dasar T yang mewakili implementasi dari tuple berdimensi variabel (sebuah "n-tuple"):

function makePairs<T extends [any, ...any]>(
  ...args:T,
): void {}
  • T extends [any, ...any] mendefinisikan sebuah tuple, berbeda dengan array, yang akan membuat beberapa pernyataan menjadi lebih mudah
  • ...args:T memperlakukan seluruh kumpulan argumen sebagai sebuah tuple

Membangun Prinsip Dasar Bedrock Kami

Kita membutuhkan beberapa tipe tuple dasar yang dapat kita gunakan nanti untuk secara dinamis memparsing tuple argumen yang lebih besar. Ini tidak terlalu kompleks untuk dipahami dan cukup dekat dengan apa yang sudah Anda miliki:

const t_string = "s" as const;
const t_number = "i" as const;
const t_boolean = "b" as const;
type StrPairTuple = [typeof t_string, string ];
type IntPairTuple = [typeof t_number, number ];
type BoolPairTuple = [typeof t_boolean, boolean ];
type PairTuple = StrPairTuple | IntPairTuple | BoolPairTuple;

Bagian pentingnya adalah PairTuple, yang akan kita gunakan berulang kali untuk mencocokkan argumen fungsi.

Masukkan Tipe Kondisional Rekursif

Gambaran Umum

Inilah saatnya hal-hal bisa mulai menjadi sedikit membingungkan. Kita akan mendefinisikan tipe VariadicTuple, tetapi penting untuk dicatat bahwa tanda tangan fungsi kita tidak akan menggunakan tipe ini secara langsung. Meskipun ini mewakili inti dari logika pencocokan tuple kita, ini juga agak kaku, jadi akan ada DynamicVariadicTuple lanjutan untuk menangani beberapa kasus tepi.

Ini adalah tipe besar dengan banyak bagian, jadi mari kita mulai dengan definisi tipe lengkapnya, kemudian kita uraikan:

type VariadicTuple<T extends [any, ...any], PT> =

    // This should always match, but we need the conditional
    // structure in order to infer the first two arguments in the
    // tuple:
    T extends [infer K, infer V, ...infer TT]
    ? [K, V] extends PT

        // If the first two arguments match the TuplePair...
        ? TT extends [any, ...any]
            // ...and there are more arguments, then recurse:
            ? [K, V, ...VariadicTuple<TT, PT>]
            // ...and there are no more arguments, then return:
            : [K, V]

        // If the first two arguments did not match, then return a
        // type that is impossible to match with an embedded error:
        : K extends PT[0]
            ? ["Unrecognized Tuple Value", never]
            : ["Unrecognized Tuple Key", never]

    // This is basically impossible to reach:
    : ["Not enough arguments", never];

Argumen Tipe Generik

VariadicTuple mengambil dua argumen tipe: T, yang merupakan n-tuple generik (extends [any, ...any]) dan PT. PT (untuk "Pair Type") adalah tipe yang mewakili tuple yang valid. Sebagai contoh, untuk mencocokkan variadic tuple dari string, number, kita akan menetapkan PT ke [string, number]. Kita akan membahas tipe ini lebih dalam nanti.

Mengekstrak Tipe Kunci, Nilai, dan Ekor

Pertama, kita menggunakan ekspresi tipe kondisional untuk mengekstrak tipe untuk dua elemen pertama dari tuple: K (untuk "Key") dan V (untuk "Value"). Elemen sisanya dari tuple (bagian "tail") disimpan dalam tipe TT agar dapat digunakan nanti untuk rekursi:

    T extends [infer K, infer V, ...infer TT]
    ? ...

Kami perlu menggunakan ekspresi kondisional untuk mengekstrak K, V, dan TT, dan ekspresi kondisional memerlukan else, meskipun yang ini tidak mungkin tercapai. Begini caranya:

    : ["Not enough arguments", never];

Menguji Apakah Kunci dan Nilai Valid

Setelah ungkapan standar itu selesai, kita dapat memulai pengujian aktual dari tipe-tipe tersebut. Di sinilah kita dapat membandingkan tipe K dan V dengan 2-tuple yang diharapkan, PT:

    ... [K, V] extends PT
        ? ...

        : K extends PT[0]
            ? ["Unrecognized Tuple Value", never]
            : ["Unrecognized Tuple Key", never]

Jika [K, V] tidak memperluas PT, maka kita memiliki pasangan tuple yang tidak valid. Mungkin tergoda untuk sekadar menyelesaikannya menjadi never atau sesuatu yang serupa pada titik ini, tetapi hal itu akan mengacaukan sisa tipe kita, menyebabkan seluruh pencocokan tipe gagal. Jika itu terjadi, maka seluruh kumpulan argumen akan dianggap salah, bukan argumen spesifik yang menyebabkan masalah.

Jadi sebagai gantinya, kami mengembalikan tuple yang tidak bisa dicocokkan. Meskipun tidak tepat merupakan kesalahan tipe standar, ini tetap akan membantu menunjukkan elemen spesifik yang memiliki tipe yang salah, apakah itu elemen pertama dari yang kelima puluh satu.

Rekursi

Jika tuple valid, maka kita perlu memvalidasi elemen yang tersisa. Untungnya, TypeScript mendukung rekursi tipe dan kita menyimpan "tail" kita di tipe TT, jadi yang perlu kita lakukan hanyalah memeriksa apakah rekursi diperlukan dan kemudian memutuskan untuk melakukan rekursi atau berhenti di sini:

        ... TT extends [any, ...any]
            ? [K, V, ...VariadicTuple<TT, PT>]
            : [K, V]

Menggabungkan VariadicTuple dan PairTuple

Definisi Fungsi

Mari kita mulai dengan definisi fungsi. Kami membuat tipe generik yang harus berupa n-tuple. Kami harus menggunakan ekspresi tipe kondisional untuk mengekstrak tipe T secara dinamis, tetapi kami akan menggunakan VariadicTuple<T, PairTuple> terlepas dari hasilnya:

function makeComplexPairs<T extends [any, ...any]>(...args:T extends VariadicTuple<T, PairTuple> ? T : VariadicTuple<T, PairTuple>):void {}

Hasil

// Success
makeComplexPairs(t_number, 1, t_boolean, true);

// Success
makeComplexPairs(t_string, "1", t_number, 1, t_number, 4); 

// Error: Expected 2 arguments, but got 1.
makeComplexPairs("g");

// Error:
makeComplexPairs("h", 107);
//               ^ Argument of type '"h"' is not assignable to parameter of type '"Unrecognized Tuple Key"'.

// Error:
makeComplexPairs("i", 107, "b", true);
//                         ^ Argument of type '"b"' is not assignable to parameter of type '"Unrecognized Tuple Key"'.

// Success: 'as const' is required because 'infer'
//          changes type "b" to type string
makeComplexPairs("i", 107, "b" as const, true);

// Error:
makeComplexPairs("i", 107, "b" as const, "bad");
//                         ^ Argument of type '"b"' is not assignable to parameter of type '"Unrecognized Tuple Value"'.

Bekerja Dengan Tuple Variadik Dinamis

Membangun Tipe Bantuan dan Definisi Fungsi

Tipe PT dapat ditentukan secara dinamis berdasarkan dua argumen pertama yang diberikan ke fungsi. Untuk mempermudah penentuan ini, saya telah membuat tipe bantuan bernama DynamicVariadicTuple. Tugasnya hanyalah menangani dua elemen pertama dari sebuah tuple, lalu menggunakan elemen-elemen tersebut sebagai tuple 2-elemen untuk PT dalam tipe VariadicTuple. Perlu diperhatikan bahwa kita masih perlu menggunakan ekspresi kondisional untuk mengekstrak K, V, dan TT, meskipun hal itu selalu berhasil:

type DynamicVariadicTuple<T extends [any, ...any]> =
    T extends [infer K, infer V, ...infer TT]
    ? TT extends [any, ...any[]]
        ? [K, V, ...VariadicTuple<TT, [K, V]>]
        : [K, V]
    : ["Not enough arguments", never];

Definisi fungsi pada dasarnya sama seperti di atas:

function makeSimplePairs<T extends [any, ...any]>(...args:T extends DynamicVariadicTuple<T> ? T : DynamicVariadicTuple<T>): void {
}

Hasil

// Success: 'infer' changes consts to simpler types,
//          like string, so we need to be explicit
//          with the first argument
makeSimplePairs("abc" as string, 123, "abc", 456);

// Success:
makeSimplePairs("ab" + "c", 123, "abc", 456);

// Success:
let abc = "abc";
makeSimplePairs(abc, 123, "def", 456);

// Error:
makeSimplePairs(abc, 123, "def", true);
//                        ^ Argument of type '"def"' is not assignable to parameter of type '"Unrecognized Tuple Value"'.

// Error:
makeSimplePairs(abc, 123, "def", 456, 789, 1000);
//                                    ^ Argument of type '789' is not assignable to parameter of type '"Unrecognized Tuple Key"'.

Catatan Tambahan

Ketidakkonsistenan Inferensi Tipe

Tampaknya ada ketidaksesuaian halus antara bagaimana TypeScript menafsirkan tipe generik untuk argumen fungsi dan bagaimana ia menafsirkan tipe dalam tipe kondisional. Pada yang pertama, ia mengambil definisi yang paling sempit, sementara pada yang terakhir tampaknya lebih memilih definisi yang lebih umum.

Sebagai contoh, jika argumen pertama yang diberikan ke makeSimplePairs() adalah "abc" (seperti yang digambarkan di atas), maka tipe yang diturunkan adalah string literal "abc", yang berarti setiap argumen lain juga harus "abc". Jika Anda bermaksud agar ini menjadi string, maka Anda perlu membuatnya lebih umum dengan melakukan casting (as string) atau cara lain (misalnya sebuah variabel, let abc="abc", atau sebuah operasi, "ab" + "c").

Ini juga memengaruhi fungsi makeComplexPairs(). Sementara argumen pertama dibaca dengan benar sebagai literal (misalnya "i", "s" atau "b"), semua argumen berikutnya dibaca sebagai tipe yang lebih umum (misalnya string), jadi Anda perlu menggunakan variabel yang bertipe ketat atau kata kunci as const.

Artikel terkait