یونینهای تایپاسکریپت بر اساس ویژگی داخل شیء تو در تو


من در تلاش هستم تا یک نوع اتحاد (Union Type) بر اساس یک ویژگی تو در تو در شیء خود ایجاد کنم. نمونه زیر را ببینید:

type Foo = {
    abilities: {
        canManage: boolean
    }
}

type Bar = {
    abilities: {
        canManage: boolean
    }
    extraProp: number
}

type Condition1 = {
    abilities: {
        canManage: true
    }
} & Bar

type Condition2 = {
    abilities: {
        canManage: false
    }
} & Foo

type TotalData = Condition1 | Condition2

const data: TotalData = {
    abilities: {
        canManage: false, // if canManage is false, TS should complain when I add the `extraProp` key
    },
    extraProp: 5
}

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

type Foo = {
     canManage: boolean
}

type Bar = {
    canManage: boolean
    extraProp: number
}

type Condition1 = {
    canManage: true
} & Bar

type Condition2 = {
    canManage: false
} & Foo
]
type TotalData = Condition1 | Condition2

const data: TotalData = {

canManage: false,
extraProp: 5 // now typescript complains that this property shouldn't be here because canManage is false
}

چگونه میتوانم این مشکل را هنگام تلاش برای تنظیم یک Union بر اساس ویژگی داخل یک شیء تو در تو حل کنم؟

پاسخ شماره ۱

کامپایلر مفهوم «اتحادیههای متمایز تو در تو» را درک نمیکند. یک نوع زمانی اتحادیه متمایز است که اعضای اتحادیه یک ویژگی «تفکیککننده» مشترک داشته باشند. ویژگی تفکیککننده معمولاً یک نوع تکمقداری/حرفهای مانند true یا "hello" یا 123 یا حتی null یا undefined است. با این حال، شما نمیتوانید از یک اتحادیه متمایز دیگر به عنوان تفکیککننده استفاده کنید. خوب بود اگر میتوانستید، زیرا در این صورت اتحادیههای متمایز میتوانستند از ویژگیهای تو در تو به سمت بالا گسترش یابند، همانطور که شما انجام میدهید. پیشنهاداتی در microsoft/TypeScript#18758 برای اجازه دادن به این موضوع وجود دارد، اما من هیچ تحرکی در آنجا نمیبینم.

همانطور که هست، نوع TotalData یک اجتماع تفکیکشده نیست. این فقط یک اجتماع است. این بدان معناست که کامپایلر تلاش نمیکند یک مقدار از نوع TotalData را بهطور انحصاری به عنوان Condition1 یا Condition2 در نظر بگیرد. بنابراین احتمالاً با مشکلاتی مواجه خواهید شد اگر کدی بنویسید که data.abilities.canManage را آزمایش کند و انتظار داشته باشید کامپایلر پیامدها را درک کند:

function hmm(x: TotalData) {
    if (x.abilities.canManage) {
        x.extraProp.toFixed(); // error!
    //  ~~~~~~~~~~~ <--- possibly undefined?!
    } 
}

اگر میخواهید این کار را انجام دهید، ممکن است متوجه شوید که نیاز دارید توابع نگهبان نوع تعریفشده توسط کاربر بنویسید:

function isCondition1(x: TotalData): x is Condition1 {
    return x.abilities.canManage;
}

function hmm(x: TotalData) {
    if (isCondition1(x)) {
        x.extraProp.toFixed(); // okay!
    } 
}

مشکل خاصی که در اینجا با آن مواجه هستید، جایی که data به عنوان یک TotalData معتبر دیده میشود، به نحوه انجام بررسی ویژگیهای اضافی مربوط میشود. نوعهای شیء در TypeScript "باز"/"قابلگسترش" هستند، نه "بسته"/"دقیق". شما مجاز هستید ویژگیهای اضافی که در تعریف نوع ذکر نشدهاند را بدون نقض نوع اضافه کنید. بنابراین، کامپایلر نمیتواند به طور کامل ویژگیهای اضافی را ممنوع کند؛ در عوض، از روشهای مبتنی بر قاعده برای تشخیص اینکه آیا چنین ویژگیهایی اشتباه هستند یا عمدی، استفاده میکند. قاعده اصلی به این صورت است: اگر شما یک شیء جدید ایجاد کنید و هر کدام از ویژگیهای آن در نوع مکانی که استفاده میشود ذکر نشده باشد، یک خطا رخ خواهد داد. در غیر این صورت خطایی رخ نخواهد داد.

اگر TotalData یک اتحاد متمایز بود، شما خطای مورد انتظار خود را روی data میگرفتید، زیرا data.abilities.canManage باعث میشد که کامپایلر data را از TotalData به Condition2 محدود کند که extraProp را ذکر نمیکند. اما این طور نیست، و بنابراین data همان TotalData باقی میماند که در واقع extraProp را ذکر میکند.

در microsoft/TypeScript#20863 پیشنهاد شده است که بررسی ویژگیهای اضافی برای اتحادهای غیرتمیزکاری شده سختگیرانهتر باشد. من تا حدودی موافقم؛ ترکیب و مطابقت ویژگیها از اعضای مختلف اتحاد به نظر نمیرسد مورد استفاده رایجی باشد، بنابراین هشدار احتمالاً مفید خواهد بود. اما باز هم، این یک مسئله دیرینه است و من هیچ پیشرفتی در این زمینه نمیبینم.


یکی از کارهایی که میتوانید برای این انجام دهید این است که درباره ویژگیهای اضافی که میخواهید از آنها محافظت کنید واضحتر باشید. یک مقدار از نوع {a: string} میتواند ویژگی b از نوع string داشته باشد، اما یک مقدار از نوع {a: string, b?: never} نمیتواند. بنابراین این نوع دوم از داشتن ویژگی از نوع b جلوگیری میکند بدون اینکه به حدس و قیاسهای کامپایلر برای بررسی ویژگیهای اضافی متکی باشد.

در مورد شما:

type Foo = {
    abilities: {
        canManage: boolean
    };
    extraProp?: never
}

بهشکلی بسیار مشابه با تعریف اصلی Foo شما رفتار خواهد کرد، اما اکنون این خطا را دریافت میکنید:

const data: TotalData = { // error!
// -> ~~~~
// Type '{ abilities: { canManage: false; }; extraProp: number; }'
// is not assignable to type 'TotalData'.
    abilities: {
        canManage: false,
    },
    extraProp: 5
}

کمپایلر دیگر نمیتواند data را با هیچ یک از Condition1 یا Condition2 تطبیق دهد، بنابراین شکایت میکند.


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