تایپاسکریپت - تعریف نوع برای یک آرایه از اشیاء از نوع اجتماع


من میخواهم یک تعریف نوع برای یک آرایه از اشیاء از یک نوع اتحاد یا نوع شمارشی ایجاد کنم. میخواهم تعریف نوع شکست بخورد اگر همه ورودیهای اتحاد بهعنوان مقدار یک کلید در آرایهای از اشیاء وجود نداشته باشند.

محیط آزمایشی تایپاسکریپت

export type SelectValue<T = any> = {
  value: T;
  label: string;
};

type options = "email" | "sms";

//ideally make this type check fail because it does not have all of the values of the options union
const values: SelectValue<options>[]  = [
    {
        value: "email",
        label: "Email",
    },
];

پاسخ شماره ۱

TypeScript نوعی داخلی متناظر با «آرایه کامل» ندارد، که در آن تضمین شود هر عضو یک نوع اجتماع (union) در جایی از آرایه وجود داشته باشد. همچنین نمیتوان بهسادگی نوع خاص خود را ایجاد کرد که به این صورت کار کند، حداقل نه اگر اجتماع تعداد زیادی عضو داشته باشد یا اگر بخواهید اجازه تکراریها را بدهید.

برای یک اتحاد با فقط چند عضو و اگر میخواهید تکراریها را ممنوع کنید، میتوانید یک اتحاد از تمام نوعهای تاپل قابل قبول ممکن ایجاد کنید، مانند [SelectValue<"email">, SelectValue<"sms">] | [SelectValue<"sms">, SelectValue<"email">] برای کد نمونه شما (اینجا را ببینید). اما این در تعداد 𝑛 اعضا بسیار بد مقیاس میشود؛ اتحاد تاپلهای ممکن که شامل یک عنصر برای هر عضو باشد خود دارای 𝑛! عضو خواهد بود (که همان فاکتوریل 𝑛 است)، که خیلی سریع بزرگ میشود. اتحادیهها در TypeScript حداکثر میتوانند حدود 100,000 عنصر داشته باشند و کامپایلر قبل از آن به طور قابل توجهی کند میشود. این بدان معناست که اگر حتی هشت عنصر در اتحادتان داشته باشید، تجربه بدی خواهید داشت.


در عوض، نزدیکترین چیزی که میتوانید در تایپاسکریپت به آن برسید این است که یک نوع عمومی بنویسید که بهعنوان محدودیت روی نوع آرایه عمل کند. یعنی هیچ نوع ExhaustiveArray<U> برای اتحاد U وجود ندارد؛ در عوض یک نوع عمومی ExhaustiveArray<T, U> وجود دارد که T extends ExhaustiveArray<T, U> تنها در صورتی برقرار است که T آرایهای باشد که تمام اعضای U را در بر گیرد. و شما به یک تابع کمکی نیاز دارید تا مانع شوید خودتان T را بنویسید. یعنی به جای const arr: ExhaustiveArray<MyUnion>=[...]، شما const arr=exhaustiveArrayForMyUnion(...) را مینویسید.

پس بیایید این را تعریف کنیم:

type ExhaustiveArray<T extends readonly any[], U> =
  [U] extends [T[number]] ? T : [...T, Exclude<U, T[number]>]

const exhaustiveArray = <U,>() => <T extends readonly U[]>(
    ...t: [...T extends ExhaustiveArray<T, U> ? T : ExhaustiveArray<T, U>]) => t as T;

اینجا ExhaustiveArray<T, U> یک نوع شرطی است که بررسی میکند آیا اجتماع U به طور کامل توسط اجتماع همه عناصر آرایه T پوشش داده شده است یا خیر. اگر اینطور باشد، مقدار آن به T ارزیابی میشود (و از آنجایی که T extends T، این موفقیتآمیز خواهد بود). اگر نه، به یک تاپل با یک عنصر بیشتر از T در انتها ارزیابی میشود، که شامل همه چیزهای گمشده است (با استفاده از نوعهای تاپل متغیر برای اضافه کردن یک عنصر و نوع کمکی Exclude برای محاسبه عناصر گمشده).

و مقدار exhaustiveArray یک تابع کمکی کوری شده است که یک نوع اتحادیه U میگیرد و یک تابع جدید تولید میکند که T را از مقدار وارد شده استنتاج میکند و سپس بررسی را انجام میدهد. این به روش عجیبی نوشته شده است؛ عالی میشد اگر میتوانستیم بنویسیم <T extends ExhaustiveArray<T, U>>(...t: [...T]) => t، اما این بهطور غیرقانونی حلقهای است. یا اگر <T extends readonly U[]>(...t: [...ExhaustiveArray<T, U>]) => t کار میکرد، عالی بود، اما کامپایلر نمیتواند T را از t به این روش استنتاج کند. نسخهی بالا باعث میشود که کامپایلر ابتدا T را از مقدار t استنتاج کند و سپس آن را به ExhaustiveArray<T, U> تبدیل کند. هدف اصلی این روش عجیب این است که هنگام وارد کردن آرایههای ناقص، یک پیام خطای «خوب» دریافت کنید. یک پیام خطای «بد» این بود که کامپایلر فقط بگوید «آن مقدار قابل اختصاص به never نیست»، که آزاردهنده است زیرا به توسعهدهنده کمکی نمیکند تا بداند چگونه میتواند مشکل را برطرف کند.

باشه، بیایید تست کنیم. اول بیایید تابع exhaustiveSelectValueArray() را بگیریم:

const exhaustiveSelectValueArray = exhaustiveArray<
  { [K in Options]: SelectValue<K> }[Options]>();

جایی که آرگومان نوع یک نوع نگاشت توزیعی است که اجتماع Options یعنی O1 | O2 | O3 | ... | ON را به SelectValue<O1> | SelectValue<O2> | SelectValue<O3> | ... | SelectValue<ON> تبدیل میکند.

و این هم این است:

const values = exhaustiveSelectValueArray(
    { value: "email", label: "Email" },
    { value: "sms", label: "SMS" }
); // okay

const err = exhaustiveSelectValueArray(
    { value: "email", label: "Email" }
); // error! Expected 2 arguments, but got 1.

const err2 = exhaustiveSelectValueArray(
    { value: "email", label: "Email" },
    { value: "email", label: "SMS" }
); // error! Expected 3 arguments, but got 2.

تماس اول موفق است زیرا هر دو عنصر ارسال شدهاند، در حالی که دو تماس بعدی ناموفق هستند زیرا یک آرگومان کم است. اگر از IntelliSense برای بررسی تماسهای تابع استفاده کنید تا ببینید کدام آرگومان کم است، هر دو نشان میدهند که یک آرگومان مورد انتظار از نوع SelectValue<"sms"> در انتها ارسال نشده است.

لینک زمین بازی به کد