جاوااسکریپت - گسترش عضو شیء از زیرکلاس


من دارم Page Objects برای تستهای خودکارم میسازم و دارم چیزی شبیه به این انجام میدهم:

abstract class Page  {
  selectors: {
    // Here are selectors which are common to all my pages
    genericSelector: "...",
    commonContainer: {
      aButton: "..."
    }
  }

  // methods that are common to all my pages
}
class MyPage extends Page {
  
  // Here I want my page specific selectors
  // *plus* my generic selectors 
  selectors: {
    ...this.selectors,
    mySpecificSelector: "..."
  }
}

کد بالا در زمان اجرا به درستی کار میکند: میتوانم به درستی از تمام سلکتورهای خود در کدم استفاده کنم. با این حال، تایپاسکریپت در MyPage.ts هشدار میدهد که:

Property 'selectors' in type 'MyPage' is not assignable to the same property in base type 'Page.

در اصل به این دلیل که تایپاسکریپت نمیداند من خصوصیت کلاس پایه را در کلاس زیرین پخش میکنم.

حالا، موارد زیر چیزی است که من امتحان کردم:

یک نکته اضافی

نوع من برای ویژگی selector به این شکل است:

type Selector = string | SelectorFactory | { selector: string, type: "css" | "xpath" }
type SelectorFactory = (...args: any[]) => string;
type SelectorTree = { [k: string]: SelectorTree | Selector }

لینک راحت TS Playground

آنچه که امتحان کردم

اولین تلاش

class MyPage extends Page {
  selectors: {
    ...this.selectors as InstanceType<typeof Page>["selectors"]
  }
}

این، سپس شکایت میکند:

'selectors' implicitly has type 'any' because it does not have a type annotation and is referenced directly or indirectly in its own initializer.

پس من باید انجام دهم:

  selectors: SelectorTree = // ...

سپس دوباره به نقطه صفر بازمیگردم، چون اعلام میکند که برخی ویژگیها از کلاس پایه گم شدهاند. اگر : SelectorTree را به کلاس پایهام اضافه کنم، دیگر شکایتی نمیکند، اما هوشمندی کد (IntelliSense) درست کار نمیکند و حتی خطا دریافت میکنم (به عنوان مثال اگر سعی کنم یک تابع از ویژگیهای سلکتورم صدا بزنم، نمیتوانم).

دومین تلاش

فکر میکنم میتوانستم عضو selectors کلاس پایه Page را استاتیک کنم. سپس میتوانستم این کار را انجام دهم:

  selectors: {
    ...Page.selectors,
    mySpecificSelector: "..."
  }

این کار میکند، اما خب، حالا واقعاً یک عضو ثابت اضافی دارم که نمیخواستم داشته باشم.

سومین تلاش، همچنین به عنوان «من از تسلیم میکنم»

من میتوانستم اعضا را جدا کنم و چیزی شبیه به baseSelectors در کلاس Page خود داشته باشم و سپس میتوانستم از آنها همانطور که هستند در زیرکلاس استفاده کنم، یا آنها را در شیء selectors خود پخش کنم.

اما این احساس شکست میدهد.

پاسخ شماره ۱

متأسفانه به نظر میرسد این یک محدودیت طراحی در تایپاسکریپت باشد، طبق microsoft/TypeScript#33899:

به دلیل محدودیتهای معماری در نحوه علامتگذاری درختان بهعنوان بررسیشده، این باید بهعنوان یک ارجاع دورانی در نظر گرفته شود با وجود اعلام نوع.

ظاهراً هر ارجاعی به this.selectors، حتی اگر غیرمستقیم باشد، به نظر میرسد که باعث خطای چرخهای میشود. تا کنون، تنها راهحلی که پیدا کردهام و نوع درست را بدون نیاز به بازسازی کد تولید شده به شما میدهد، این است که this را به Page گسترش دهید (توجه: InstanceType<typeof Page> با Page یکسان است) تا نوع غیر-any برای this.selectors به دست آید، و سپس به صورت مستقیم با //@ts-ignore هشدار چرخهای را سرکوب کنید:

class SubPage extends Page {
  //@ts-ignore circularity warning
  selectors = { ...(this as Page).selectors, foo: "bar", open: () => "" }
}

میتوانید بررسی کنید که نمونههای SubPage همانطور که انتظار میرود رفتار میکنند:

const p = new SubPage();
p.selectors;
// SubPage.selectors: {
//   foo: string;
//   open: () => string;
//   test: string;
// }
p.selectors.open().toUpperCase(); // okay

این کار میکند! اما...

⚠ هشدار ⚠ من هر وقت کسی پیشنهاد //@ts-ignore میدهد، احساس ناراحتی شدیدی دارم، چون اگر خطای سرکوب شده تأثیرات ناخوشایند دیگری روی کد شما داشته باشد، آنها هنوز هم وجود خواهند داشت. من هیچ یک از این تأثیرات را در کد نمونه بالا مشاهده نکردم، اما نمیتوانم مطمئن باشم که وجود ندارند. اگر مکانیک خودرو شما چراغ «بررسی موتور» روشن را با پوشاندن چراغ طوری که دیگر دیده نشود برطرف کند، شاید بخواهید شروع به جستجو برای پیدا کردن مکانیک دیگری کنید، حتی اگر فقط برای یک نظر دوم باشد. 😅

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