نوع پیشبینی تایپاسکریپت که از طریق عناصر تاپل نگاشته شده است


یک اجتماع از دو نوع را با یک پیشبینی نوع در نظر بگیرید

type Maybe<T> = T | Error
function isValid<T>(value: Maybe<T>): value is T {
  return !(value instanceof Error)
}

بیایید کدی داشته باشیم با چندین مقدار محاسبهشده Maybe و ما میخواهیم یک گزارهی نوع که به طور خلاصه همهٔ آنها را بررسی کند تا نباشند یک Error:

const a: Maybe<string> = ... // some function returning Maybe<string>
const b: Maybe<number> = ...
...

if (![a, b, /*and more*/].every(isValid)) {
  return;
}
const realA: string = a;
//Type 'Maybe<string>' is not assignable to type 'string'.
//  Type 'Error' is not assignable to type 'string'.(2322)

زمین بازی

با وجود every(isValid)، کامپایلر نمیتواند نتیجه بگیرد که هیچیک از a، b، ... دیگر یک Maybe نیستند.

البته که میتوانم بنویسم

if (!isValid(a) && !isValid(b) && ...) {
  return;
}

برای کمک به کامپایلر. اما من ۵ تا ۱۰ a و b دارم و به طور کلی کنجکاو هستم که آیا راهحلی وجود دارد. اگر امتحان کردهام

const data = [a, b].filter(isValid)

اما این یک نوع آرایه نامتناسب ایجاد میکند مانند (string | number)[]، بنابراین data[0] همچنان به طور قاطع یک string نیست.

پس نه every و نه filter ترفند اجازه دادن به کامپایلر برای حذف Maybe بعد از آن را انجام میدهند، اما من کنجکاوم که آیا راهی جمع و جور و «حلقهوار» وجود دارد که از یک دنباله دست و پاگیر if(!isValid(a)... اجتناب کند.

پاسخ شماره ۱

در حال حاضر هیچ راهی برای استفاده از یک تابع محافظ نوع سفارشی برای محدود کردن فقط محتویات یا اعضای یک مقدار وجود ندارد.

توابع نگهبان نوع سفارشی، که یک قاعده نوع به صورت arg is Type یا this is Type بازمیگردانند، فقط نوع ظاهری آرگومان خاصی که با arg مطابقت دارد یا شیئی که با this مطابقت دارد را محدود میکنند. هرگونه اثر محدودسازی تنها از طریق دسترسیهای بعدی به همان مقدار یکسان قابل مشاهده خواهد بود. توابع نگهبان نوع سفارشی بهصورت جداگانه نوع ظاهری مقادیری را که به اعضای arg یا this اختصاص یافتهاند، تحت تأثیر قرار نمیدهند. بهعنوان مثال:

declare function g(
  x: { a: RegExp | Date }
): x is { a: Date };
const a = Math.random()<0.5 ? new Date() : new RegExp("")
//    ^? const a: RegExp | Date
const x = { a };
//    ^? const x: { a:RegExp | Date; }
if (g(x)) {
  x.a;
  //^? (property) a: Date
  a;
//^? const a: RegExp | Date
}

در متن بالا، فراخوانی g(x) تأثیر محدود کردن نوع ظاهری x به {a: Date} را دارد، و بنابراین نوع ظاهری x.a به Date محدود میشود. اما a، که اشارهای به همان مقدار x.a است، محدود نشده است.

این بدان معناست که استفاده از یک تابع محافظ نوع سفارشی روی یک شیء ناشناس یا یک آرایه به طور اساسی بیفایده است:

if (g({ a })) {
  a;
  //^? const a: RegExp | Date
  ({ a }).a
  //      ^? (property) a: RegExp | Date
}

شیء لفظی { a } نمیتواند دوباره دسترسی پیدا کند؛ این ناشناس است. نوشتن { a } بار دوم فقط یک شیء جدید به ما میدهد. و ما میدانیم که خود a محدود نمیشود. بنابراین ما گیر کردهایم.

در microsoft/TypeScript#46184 یک درخواست ویژگی باز وجود دارد که درخواست پشتیبانی برای محدود کردن محتوای اشیاء (بهخصوص متغیرهای بازسازیشده از اشیاء؛ احتمالاً برای آرایهها نیز) را دارد، اما فعلاً این جزئی از زبان نیست.

--

شما در تلاش بودید تا یک آرایه صریح [a, b, ⋯] را با امضای فراخوانی محافظ نوع برای متد every() محدود کنید، اما همانطور که همین حالا مشاهده کردیم، این هیچ تأثیری نخواهد داشت:

const now = new Date().getTime();
const a: Maybe<string> = now > 1 ? 'a' : new Error;
const b: Maybe<number> = now > 1 ? 1 : new Error;
const c: Maybe<null> = now > 1 ? null : new Error;
if ([a, b, c].every(isValid)) {
  a.toUpperCase(); // error, Maybe<string> 😢
}

خوبه، خب، اگر اول مقداردهی آرایه را به یک متغیر اختصاص دهیم و بعداً اعضا را از طریق شاخصها دسترسی پیدا کنیم چه؟ این مشکل ناشناس بودن را حل میکند:

const arr = [a, b, c];
// const arr: (string | number | Error | null)[]
if (arr.every(isValid)) {
  const newA = arr[0]; // const newA: string | number | null
}

پس، ما به باریک شدن رسیدیم، خوشی! تا حدی. متأسفانه آرایه arr از نوع Maybe<string | number | null>[] است. بنابراین حتی اگر arr[0] را به string | number | null محدود کرده باشیم، کاملاً ترتیب ورودیها را از دست دادهایم. این ما را به مشکل بعدی میرساند:


برای پیگیری ترتیب عناصر arr، ما نیاز داریم که به جای آن دارای نوع تاپل باشد. و every به عنوان نگهبان نوع عنصر به عنصر روی تاپلها عمل نمیکند:

const tup = [a, b, c] as const;
// const tup: readonly [Maybe<string>, Maybe<number>, Maybe<null>]
if (tup.every(isValid)) {
  tup // const tup: readonly [Maybe<string>, Maybe<number>, Maybe<null>] 😟
}

و هیچ راهی برای رفع آن وجود ندارد. در حال حاضر تایپاسکریپت فاقد قابلیت بیان لازم برای توضیح نحوه انجام محافظتهای نوع عنصر به عنصر روی تاپلها است. این به روشی برای صحبت درباره یک نوع جنریک دلخواه F در سطح نوع نیاز دارد، شاید شبیه:

// NOT VALID TS, DO NOT TRY THIS
function every<T extends any[], F extends 🤷♂️>(
  arr: T, cb: <U extends T[number]>(v: U, i: number, a: T) => value is F<U>
): {[I in keyof T]: F<T[I]>}

اما شما نمیتوانید این کار را انجام دهید. F یک پارامتر نوع عمومی است که مانند یک نوع مشخص رفتار میکند؛ شما نمیتوانید آن را با آرگومانهای نوع خودش مانند F<U> یا F<T[I]> نمونهسازی کنید. یک مسئله قدیمی و باز در microsoft/TypeScript#1213 وجود دارد اما بخشی از زبان نیست. راههای مختلفی برای تلاش در شبیهسازی/رمزگذاری چنین نوعهایی وجود دارد، اما آنها به اندازه کافی کاربرپسند نیستند که اینجا پیشنهاد شوند.

برای جزئیات بیشتر در مورد یک مسئله مشابه، به «نگاه کنید: نگاشت مقدار نوع تاپل به مقدار نوع تاپل دیگر بدون تبدیل» مراجعه کنید.

بنابراین ما باید از استفاده از نوعی نگهبان نوع عمومی که isValid را به عنوان ورودی میگیرد صرفنظر کنیم و فقط پیش برویم و isValid() را در آنجا بهصورت ثابت برنامهنویسی کنیم.


اگر بخواهیم از یک آرایه استفاده کنیم، میتوانیم بنویسیم

function isEveryElementValid<T extends readonly any[]>(
  values: { [K in keyof T]: Maybe<T[K]> }): values is T {
  return values.every(isValid);
}

و سپس از آن مانند

if (isEveryElementValid(tup)) {
  const realA = tup[0]; // const realA: string
  const realB = tup[1]; // const realB: number
  const realC = tup[2]; // const realC: null
}

یا اگر بخواهیم از یک شیء استفاده کنیم، میتوانیم بنویسیم

function isEveryPropertyValid<T extends object>(
  values: { [K in keyof T]: Maybe<T[K]> }): values is T {
  return Object.values(values).every(isValid);
}

و سپس از آن مانند

const o = {a,b,c};
if (isEveryPropertyValid(o)) {
  const realA = o.a; // string
  const realB = o.b; // number
  const realC = o.c; // null
}

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