پیادهسازی رابط TypeScript با عضو شیء پارامتر اضافی خطای کامپایل مورد انتظار را ایجاد نمیکند
انواع زیر را در نظر بگیرید:
type User = {
id: string;
name: string;
};
interface UsersRepository {
getUser(query: { id: string }): User;
}
class Repo implements UsersRepository {
getUser({ id, name }: { id: string; name: string }) {
return { id, name: name.toUpperCase() };
}
}
const illustrateProblem = () => {
const repo: UsersRepository = new Repo();
console.log(repo.getUser({ id: "foo" }));
};
illustrateProblem();
توجه داشته باشید که روش پیادهسازی به دو ویژگی در شی ورودی نیاز دارد؛ id و name، در حالی که رابط تنها به id نیاز دارد.
این هیچ خطای کامپایل ایجاد نمیکند!
با این حال، در زمان اجرا، این کد به خطا خواهد خورد زیرا فراخوانی repo.getUser هیچ name ای را در آرگومان خود ارائه نمیدهد، بنابراین undefined خواهد بود و name.toUpperCase() خطا تولید میکند. به عبارت دیگر، name در زمان اجرا undefined است، حتی اگر کامپایلر TypeScript فکر کند که هرگز نمیتواند اینطور باشد.
چرا تایپاسکریپت این را اجازه میدهد؟
توضیح: میدانم که این پیادهسازی LSP را نامعتبر میکند، و نکته همین است: انتظار داشتم TypeScript برای انجام این کار به من هشدار دهد، با دادن خطا در اعلام پیادهسازی getUser که این متد نمیتواند اینترفیس را پیادهسازی کند، زیرا به فیلدهایی در آرگومان خود نیاز دارد که در امضای اینترفیس تعریف نشدهاند.
پاسخ شماره ۱
مشکلی که مشاهده میکنید به دلیل دوگانگی پارامتر متد در تایپاسکریپت است. این یکی از جاهایی است که سیستم نوعبندی ناپایدار است و به شما اجازه میدهد کارهای ناایمن انجام دهید. ظاهراً کدهای واقعی زیادی وجود دارد که زیرکلاسها انتظار دارند متدها پارامترهایی خاصتر را بپذیرند به جای پارامترهای کلیتر. تایپاسکریپت این را با پذیرش عمداً پارامترهای گستردهتر (ایمن) یا باریکتر (ناایمن) پشتیبانی میکند و متأسفانه شما دقیقاً با این مسئله برخورد کردهاید.
خوشبختانه، از زمان TypeScript 2.6 یک فلگ کامپایلر --strictFunctionTypes وجود دارد که بایوارینس آرگومان تابع را غیرفعال میکند (و فقط کانتروارینس را اجازه میدهد). متأسفانه، حتی با روشن بودن این فلگ، این تنها به توابع مستقل اعمال میشود و نه متدهای کلاس. خوشبختانه، با یک اینترفیس در واقع یک راه (که خیلی خوب مستند نشده) وجود دارد تا امضای یک متد را از امضای یک ویژگی تابع-مقدار متمایز کرد:
interface Foo<A, R> {
methodSignature(arg: A): R;
functionSignature: (arg: A) => R;
}
هر دو methodSignature و functionSignature به چیزهایی شبیه تابع اشاره دارند که به یک Foo متصل هستند، و در عمل میتوانید هر کدام را با یک متد مبتنی بر نمونه یا با یک خاصیت تابعی که مقداری نمونه دارد پیادهسازی کنید. و خوشبختانه، امضای خاصیت تابع از --strictFunctionTypes پیروی خواهد کرد. بنابراین من تعریف UsersRepository را به این صورت تغییر میدادم:
interface UsersRepository {
getUser: (a: { id: string }) => User;
}
و ناگهان خطایی را که انتظار داشتید خواهید دید:
class Repo implements UsersRepository {
getUser({ id, name }: { id: string; name: string }) {
// error! Property 'name' is missing in type '{ id: string; }'
// but required in type '{ id: string; name: string; }'.
return { id, name: name.toUpperCase() };
}
}
و بنابراین میتوانید آن را درست کنید:
class FixedRepo implements UsersRepository {
getUser({ id, name }: { id: string; name?: unknown }) {
return {
id,
name: (typeof name === "string" ? name : "nobody").toUpperCase()
};
}
}
const illustrateProblem = () => {
const repo: UsersRepository = new FixedRepo();
console.log(repo.getUser({ id: "foo" }));
};
illustrateProblem(); // { id: "foo", name: "NOBODY" }
خب، امیدوارم مفید باشد؛ موفق باشید!
لینک به کد
