تایپاسکریپت - خطا هنگام اختصاص نوع سازگار به یک نوع اتحادیه متمایز


من یک مثال مورد کاهش یافته دارم. من یک اتحادیه تفکیکشده AOrB ایجاد میکنم، و یک نوع (به نظر من) سازگار C:

interface A {
  kind: 'a';
}

interface B {
  kind: 'b';
}

type Kind = 'a' | 'b';

interface C {
  kind: Kind;
}

interface D {
  kind: 'b';
}

type AOrB = A | B;

function test(input: C): AOrB {
  return input;
}

function test2(input: D): AOrB {
  return input;
}

ناکام میشود با

Type 'C' is not assignable to type 'AOrB'.
  Type 'C' is not assignable to type 'B'.
    Types of property 'kind' are incompatible.
      Type 'Kind' is not assignable to type '"b"'.
        Type '"a"' is not assignable to type '"b"'.

این باعث میشود که به نظر برسد من نمیتوانم یک شیء را به یک متغیر از نوع AOrB اختصاص دهم مگر اینکه از قبل معلوم باشد که آیا در واقع A است یا یک B. فقط این نیست که آنها نیاز به نوع دقیق یکسان داشته باشند، زیرا test2 بدون مشکل کامپایل میشود.

آیا کسی میتواند توضیح دهد که چه خبر است؟

پاسخ شماره ۱

بهروزرسانی: ۲۰۱۹-۰۵-۳۰ با انتشار TypeScript 3.5 این مشکل باید توسط بررسی هوشمندتر نوع اتحاد برطرف شود. موارد زیر برای نسخه ۳.۴ و پایینتر اعمال میشود:

این مشکل قبلاً گزارش شده است و اساساً یک محدودیت کامپایلر است. در حالی که تو و من میدانیم که {a: X} | {a: Y} معادل است با {a: X | Y}، اما کامپایلر این را نمیداند.

به طور کلی شما نمیتوانید اتحادیههای اشیاء را به اشیاء اتحادیه ترکیب کنید مگر اینکه اشیاء تنها در نوع یک ویژگی با یکدیگر تفاوت داشته باشند (یا دقیقتر، ویژگیهای متفاوت نمایانگر تمام توزیعهای ممکن اتحادیهها باشند). برای مثال، نمیتوانید {a: X, b: Z} | {a: Y, b: W} را به چیزی مانند {a: X | Y, b: Z | W} تبدیل کنید... نوع {a: X, b: W} به نسخه دوم قابل انتساب است اما به نسخه اول نه.

تصور کنید نوشتن کامپایلری که سعی کند چنین کاهشهایی را انجام دهد؛ در اکثر موارد، زمان ارزشمند پردازنده را صرف بررسی نوعهای اتحاد میکند تنها برای اینکه بفهمد نمیتواند آنها را ترکیب کند. موارد نسبتاً نادری مانند مورد شما که کاهش ممکن است، احتمالاً ارزشش را ندارد.

حتی در مورد شما، من مشکوکم؛ آیا واقعاً سعی دارید یک اتحاد تبعیضی بسازید که در آن تنها ویژگی تبعیضی متفاوت باشد؟ این یک مورد غیرعادی است. مورد استفاده مورد نظر برای اتحادات تبعیضی این است که مجموعهای از نوعهای مختلف بگیرید و یک ویژگی واحد اضافه کنید تا بدانید یک مقدار از چه نوعی است. به محض این که شروع به افزودن سایر ویژگیها کنید که نوعهای تبعیضی را واقعاً متفاوت میکنند، باید ایده تجمیع اتحاد را رها کنید.

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

function test(input: C): AOrB {
  return input as AOrB; // you know best
}

این اکنون کامپایل میشود، اگرچه واقعاً ایمن نیست:

function test(input: C): AOrB {
  return (Math.random() < 0.5 ? input : "whoopsie") as AOrB; // also compiles
}

یک راهحل ایمنتر اما پیچیدهتر، راهنمایی کامپایلر از طریق احتمالهای مختلف است:

function test(input: C): AOrB {      
  return input.kind === 'a' ? {kind: input.kind} : {kind: input.kind};
}

هر کدام از اینها یا چیز دیگری که به ذهن شما میرسد باید کار کند.

خب، امیدوارم مفید باشد؛ موفق باشی!

پاسخ شماره ۲

پس بیایید این را قدمبهقدم پیش ببریم. بیایید A و B را به عنوان اینترفیسها و Kind را به عنوان یک نوع جداگانه حذف کنیم و تمام موارد استفاده از آن اینترفیسها را با مقادیر دقیقشان جایگزین کنیم:

interface C {
  kind: 'a' | 'b';
}

interface D {
  kind: 'b';
}

type AOrB = { kind: 'a' } | { kind: 'b' }

function test(input: C): AOrB {
  return input;
}

function test2(input: D): AOrB {
  return input;
}

حالا، به روشی مشابه، بیایید از C، D و AOrB خلاص شویم:

function test(input: { kind: 'a' | 'b' }): { kind: 'a' } | { kind: 'b' } {
  return input;
}

function test2(input: { kind: 'b' }): { kind: 'a' } | { kind: 'b' } {
  return input;
}

حال، همانطور که میبینید در test2، تابع باز میگرداند { kind: 'a' } | { kind: 'b' }. input از نوع { kind: 'b' } است، بنابراین به طور طبیعی با آن تعریف مطابقت دارد.

در test1 تابع مقدار { kind: 'a' } | { kind: 'b' } را برمیگرداند، اما ورودی از نوع { kind: 'a' | 'b' } است. اگرچه وسوسهانگیز است که بگوییم آن تعاریف با هم مطابقت دارند، اما یک شیء که یک ویژگی از نوع 'a' یا نوع 'b' دارد، قابل انتساب به هر یک از اشیاء با یک ویژگی از نوع 'a' یا یک شیء با یک ویژگی از نوع 'b' نیست. نوع { kind: 'a' | 'b' } به شما اجازه میدهد کاری شبیه این انجام دهید:

let value: { kind: 'a' | 'b' } = {
    kind: 'a'
};
value.kind = 'a';
value.kind = 'b';

حال، { kind: 'a' | 'b' } را با { kind: 'a' } | { kind: 'b' } جایگزین کنید:

let value: { kind: 'a' } | { kind: 'b' } = {
    kind: 'a'
};
value.kind = 'a';
value.kind = 'b';

متغیر value با مقدار از نوع { kind: 'a' } مقداردهی اولیه شد، بنابراین کامپایلر فرض میکند که این نوع مقداری است که این متغیر اکنون در خود دارد. در این صورت، اختصاص دادن b به ویژگی kind غیرقانونی در نظر گرفته خواهد شد.