مشکل استفاده از یک نوع عمومی تعریفشده در TypeScript در یک تابع خارجی با استفاده از نوع T
من در تلاش هستم تا انواع عمومی بیشتری به کتابخانه متن باز خود Angular-Slickgrid اضافه کنم که یک کتابخانه شبکه داده است.
من یک اینترفیس برای تعریف ستونها تعریف کردم که میتواند چندین نوع Column داشته باشد، من یک نوع عمومی جدید به این اینترفیس Column اضافه میکنم و میخواهم از این نوع جدید در گزینه formatter استفاده کنم، که خود از نوع Formatter است، همانطور که در زیر نشان داده شده است. این نوع عمومی جدید اساساً برای این است که به Formatter بگوید نوع شیء آیتم میتواند چه باشد.
export interface Column<T = any> {
field: string;
// oops that was a typo
// formatter?: Formatter: T;
// correct code is actually this
formatter?: Formatter<T>;
}
کلاس Formatter به صورت زیر تعریف شده است
// add Generic Type for the item object
export declare type Formatter<T = any> = (row: number, cell: number, value: any, columnDef: Column, item: T) => string;
حالا من دارم تلاش میکنم از جنریکهای جدید استفاده کنم با ایجاد یک رابط ReportItem که به columnDefinitions منتقل میشود تا بتوانم (امیدوارم) از نوع ReportItem در داخل Formatter خود استفاده کنم
interface ReportItem {
title: string;
duration: number;
cost: number;
percentComplete: number;
start: Date;
finish: Date;
effortDriven: boolean;
}
const customEditableInputFormatter: Formatter = <T = any>(row: number, cell: number, value: any, columnDef: Column, item: T) => {
// I want the `item` to be of Type ReportItem but it shows as Type any
// item.title
};
export class MySample {
columnDefinitions: Column<ReportItem>[];
initializeGrid() {
this.columnDefinitions = [
{
id: 'title', name: 'Title', field: 'title', sortable: true, type: FieldType.string,
formatter: customEditableInputFormatter,
}
];
}
}
اگر ماوس را روی ویژگی formatter ببرم، Formatter<ReportItem> صحیح را با تکمیل خودکار (در VSCode) میبینم.
اما اگر موس را روی خود قالبدهندهٔ سفارشی قرار دهم، Formatter<any> را دریافت میکنم
سؤال من این است که چگونه میتوانم آن را طوری کدنویسی کنم که واقعاً نوع صحیح ReportItem را در Formatter خارجی من بگیرد، من میخواهم Formatter خارجیام از نوع Formatter<ReportItem> باشد به جای Formatter<any>. من تازه شروع به یادگیری جنریکها کردهام و آنها را بسیار قدرتمند میبینم اما هنوز راه زیادی برای یادگیری دارم.
از هر کمکی که ارائه شده سپاسگزارم.
ویرایش ۱
من همچنین سعی کردم <T=any> را با <T> در فرمتکنندهٔ سفارشی (const customEditableInputFormatter: Formatter=<T>...) جایگزین کنم و هنگام استفاده از آن، customEditableInputFormatter as Formatter<ReportItem> را اضافه کنم، اما هنوز نشان میدهد که T همان any است.
پاسخ شماره ۱
مرور کلی
به نظر میرسد مشکل عمدتاً مربوط به استفاده از نحو صحیح جنریکها باشد، که به نظر میرسد ممکن است نشاندهنده یک سوء تفاهم جزئی در هنگام یادگیری مفاهیم جدید باشد.
من کد شما را به شکل قابل اجرا بازنویسی کردهام. به دقت نگاه کنید که پارامترهای عمومی نسبت به دونقطه و عملگرهای انتساب کجا ظاهر میشوند، زیرا میتواند خطاها را بسیار ظریف کند. کامپایلر TypeScript کمک نمیکند، زیرا اعلانهای ارائه شده هنوز هم سینتکس معتبری دارند (به جز موجدارها در item.title در نسخههای غیرقابل اجرا)، فقط سینتکس صحیح برای آنچه شما میخواهید به دست آورید نیست. TypeScript منظور شما را نمیفهمد، فقط آنچه انجام دادهاید را میفهمد :-)
ستون<T>
ابتدا بیایید آن تعریف Column<T> را اصلاح کنیم تا property formatter به درستی نوعدهی شود. ممکن است فقط یک اشتباه تایپی باشد، اما خواستم به آن اشاره کنم. (همچنین ویژگیهای گمشدهای را که در متد MySample.initializeGrid() شما استفاده شدهاند، اضافه کردم):
export interface Column<T = any> {
field: string;
id: number | string;
name?: string;
sortable?: boolean;
type?: FieldType;
formatter?: Formatter<T>; // originally formatter?: Formatter: T;
}
Formatter<T>
حال، این تعریف عمومی Formatter<T> شما است، که خوب است:
export declare type Formatter<T = any> = (
row: number, cell: number, value: any, columnDef: Column, item: T
) => string;
با این حال، بر اساس تعریف Column<T> شما که ویژگی formatter را با T پارامتر میکند، به نظر میرسد که شما ممکن است بخواهید آرگومان columnDef را با همان پارامتر عمومی T که با item مطابقت دارد مشخص کنید، مانند این:
export declare type Formatter<T = any> = (
row: number, cell: number, value: any, columnDef: Column<T>, item: T
) => string;
اما این جزئیات اضافی است که شما باید تصمیم بگیرید. حالا به مشکل اصلی میپردازیم...
راهحل customEditableInputFormatter (خلاصه)
اول، قبل از پرداختن به جزئیات زیادی از نحو زبان TypeScript، اجازه دهید نسخهٔ اجرایی یکخطی و مختصر TLDR را ارائه دهم. این نسخه از استنتاج نوع TypeScript استفاده میکند، بنابراین همهٔ آرگومانها و مقدار بازگشتی بر اساس اعلام آن بهعنوان Formatter<ReportItem> بهصورت قوی تایپ شدهاند:
const customEditableInputFormatter: Formatter<ReportItem> = (r, c, v, d, i) => i.title;
شما میتوانید نام کامل آرگومانها را بنویسید، تمام نوعهای آنها را مشخص کنید، نوع بازگردانی را مشخص کنید و براکتهای منحنی حول بدنهٔ تابع اضافه کنید، اما این همه چیزی است که به طور دقیق برای نوعبندی قوی، IntelliSense و غیره لازم است. و این هم فرم کمی طولانیتر که به یک IDE تکیه نمیکند تا به خواننده بگوید نوعها چیستند:
const customEditableInputFormatter: Formatter<ReportItem> = (
row: number, cell: number, value: any, columnDef: Column<ReportItem>, item: ReportItem
) => {
return item.title; // item is type `ReportItem`
}
اگر برای اطمینان از تطابق امضا با Formatter<T> برای کمک به جلوگیری از خطا هنگام ایجاد تابع اهمیتی نمیدهید، میتوانید حتی آن را بدون اعلان به عنوان Formatter<ReportItem> به این شکل کوتاه کنید:
const customEditableInputFormatter = (
row: number, cell: number, value: any, columnDef: Column<ReportItem>, item: ReportItem
) => {
return item.title; // item is type `ReportItem`
}
TypeScript همچنان اجازه میدهد که بعداً به Formatter<T> اختصاص داده شود، اما مستعد خطا است و من آن را توصیه نمیکنم.
تحلیل خطای customEditableInputFormatter
خب، با این از سر راه برداشته شده، بیایید کد شما را با جزئیات بیشتری بررسی کنیم...
این نسخهٔ اصلی شما از اعلامیهٔ customEditableInputFormatter است:
const customEditableInputFormatter: Formatter = <T = any>(
row: number, cell: number, value: any, columnDef: Column, item: T
) => {
// I want the `item` to be of Type ReportItem but it shows as Type any
// item.title // <-- oops, item is type `T` here, i.e. `any`
};
چند نکته در این نسخه وجود دارد که باید به آنها توجه کرد:
- در سمت چپ عملگر انتساب (اولین علامت مساوی)، میبینید که نوع آن را به عنوان
Formatterاعلام کردهاید بدون اینکه در واقع آرگومان نوعTرا مشخص کنید. بنابراین، به صورت پیشفرض بهanyتنظیم میشود، همانطور که در اعلام نوع مشخص شده است (<T=any>)، و هرگز چیز دیگری نخواهد بود. - در سمت راست عملگر انتساب، شما
<T=any>را درست قبل از باز کردن پرانتز تابع دارید. این پارامتردهی نوعFormatterدر سمت چپ نیست - این قبلاً در #1 مشخص شده است. این یک پارامتر عمومی برای تابع ناشناس جدیدی است که در سمت راست ایجاد کردهاید! اینها دوTمتفاوت هستند که فکر میکنم موجب سردرگمی شده است! این اعلان با انتساب بهFormatter<any>سازگار است، بنابراین تایپاسکریپت شکایتی ندارد. - بهطور شگفتانگیز، تغییر #2 به
<T=ReportItem>هنوز مشکل را حل نمیکند، حتی با اینکه وقتی موس را رویitemدر بدنه تابع میبرید، گفته میشود نوع آنReportItemاست! چگونه ممکن است؟ این کمی پیچیده است، اما دلیلش این است که بدنه تابع باید از تمام نوعهای ممکنTپشتیبانی کند، نه فقط زمانی کهTمقدار پیشفرضReportItemدارد. TypeScript نمیتواند بداند که هرTتصادفی یک ویژگیtitleخواهد داشت، بنابراین اجازه نمیدهد از آن رویitemاستفاده کنید حتی اگر ما بدانیم که در این مورد یک ویژگی دارد. در ابتدا ضد شهودی به نظر میرسد، اما وقتی کمی درباره آن فکر کنید منطقی میشود. به هر حال، روش بهتر برای اعلان آن، نسخهای است که بالاتر کار میکند و این مشکل در آن وجود ندارد.
یک پیشنهاد
در نهایت، پیشنهاد میکنم تا جایی که میتوانید، همه پارامترهای نوع پیشفرض = any را حذف کنید. فکر میکنم که این موضوع نقش بزرگی در مخفی کردن مشکل ایفا میکرد چون مانع از این میشد که TypeScript به طور مستقیم بیشتر کمک کند. شما اشاره کردید که میخواهید به یک کدبیس موجود نوع اضافه کنید، پس ممکن است این کار چالشبرانگیزتر باشد. با این حال، حذف آنها به سرویس زبان اجازه میدهد تا مسائلی را که نوعهای پیشفرض مخفی میکنند، مشخص کند. آنها میتوانند زمانی مفید باشند که با دقت استفاده شوند، اما مگر اینکه به صراحت نیاز باشد، میتوان از آنها صرفنظر کرد.
باشه، این واقعاً زیاد است و هنوز بسیاری از مسائل دیگر ممکن است درباره انواع عمومی مانند این وجود داشته باشد که میتوان درباره آنها صحبت کرد. با این حال، من اینجا پایان میدهم چون سؤال پاسخ داده شده است. امیدوارم این مفید باشد و ادامه مطالعه و استفاده شما از انواع عمومی در تایپاسکریپت را تشویق کند!
پاسخ شماره ۲
بیایید customEditableInputFormatter را به یک تابع مرتبه بالاتر تبدیل کنیم:
const customEditableInputFormatter: <T>() => Formatter<T> = () =>
<T>(row: number, cell: number, value: any,
columnDef: Column, item: T) => '';
تابع مرتبه بالای بالا یک تابع دیگر را برمیگرداند که به نوبه خود یک رشته برمیگرداند (میدانم که این کاملاً بیفایده است، اما میتواند نشان دهد که من در VSCode چه چیزی دارم با این کار):
در تصویر بالا، میتوانید نوع را مشاهده کنید که در نوع بازگشتی تابع مرتبه بالاتر قرار گرفته است، به جای اینکه صرفاً Formatter<any> باشد.
این رویکرد به وضوح کد را برای به نفع IntelliSense محیط توسعه بیش از حد پیچیده میکند، پس به نظر من، گزینه خوبی برای استفاده در کد شما نیست. اما شاید بتواند کمی روشنایی روی مشکل بیندازد تا به پیدا کردن یک راهحل خوب کمک کند.




