جاوااسکریپت - اعلان نوع برای تابعی که یک سازنده کلاس برمیگرداند
من در حال نوشتن یک فایل اعلان TypeScript برای یک موتور بازی جاوااسکریپت با یک ساختار جهانی هستم و دقیقا نمیدانم چگونه با این تابع که یک سازنده کلاس برمیگرداند برخورد کنم. در اینجا یک نسخه سادهشده از کد آمده است:
var createScript = function(name) {
var script = function(args) {
this.app = args.app;
this.entity = args.entity;
}
script._name = name
return script;
}
قرار است کاربران سازنده کلاس script را با کد خودشان به این صورت توسعه دهند:
var MyScript = createScript('myScript');
MyScript.someVar = 6;
MyScript.prototype.update = function(dt) {
// Game programming stuff
}
راه صحیح برخورد با تابع createScript و سازنده کلاس script در فایل اعلامیه من چیست؟
بهروزرسانی من نمونه کد خود را بهروزرسانی کردهام تا نشان دهم که انتظار میرود کاربران طرف استاتیک «کلاس» را نیز گسترش دهند. پاسخهایی که تاکنون داده شدهاند، هرچند عالی هستند، به نظر نمیرسد اجازه این کار را بدهند.
پاسخ شماره ۱
شما میتوانید نوع بازگشتی createScript را به عنوان نوعی سازنده اسکریپت عمومی اعلام کنید
interface AbstractScript {
app;
entity;
}
interface ScriptConstructor<T extends AbstractScript> {
new(args: AbstractScript): T;
readonly prototype: T;
}
declare function createScript(name: string): ScriptConstructor<any>;
و سپس
interface Foo extends AbstractScript {
foo(): void;
}
let Foo:ScriptConstructor<Foo> = createScript("Foo");
Foo.prototype.foo = function () { }
let d = new Foo({app: "foo", entity: "bar"});
d.foo();
d.entity
d.app
توجه: برای راحتی، سازنده و رابط کلاس ایجاد شده هر دو را Foo نامگذاری کردهام.
اول فکر کردم که ممکن است TypeScript با این مشکل داشته باشد، اما نه، به خوبی تفاوت قائل میشود.
پاسخ شماره ۲
در تایپاسکریپت، یکی از راههای ممکن برای نمایش یک سازنده کلاس، استفاده از یک اینترفیس با چیزی که به آن امضای سازنده گفته میشود، است. چون createScript قرار است یک نمونه از کلاس ایجاد شده توسط کاربر (یا دقیقتر، کلاس تغییر یافته توسط کاربر) را برگرداند، باید عمومی (generic) باشد. کاربر باید یک اینترفیس ارائه دهد که کلاس توسعهیافته را بهعنوان پارامتر عمومی برای createScript توصیف کند:
export interface ScriptConstructorArgs {
app: {}; // dummy empty type declarations here
entity: {};
}
export interface Script { // decsribes base class
app: {};
entity: {};
}
export interface ScriptConstructor<S extends Script> {
_name: string;
new(args: ScriptConstructorArgs): S;
}
// type declaration for createScript
declare var createScript: <S extends Script>(name: string) => ScriptConstructor<S>;
هنگام استفاده از createScript، کاربران باید کلاس توسعه یافته را توصیف کنند و پیادهسازی آن را بهطور جداگانه با اختصاص دادن به prototype ارائه دهند
interface MyScript extends Script {
update(dt: {}): void;
}
var MyScript = createScript<MyScript>('myScript');
MyScript.prototype.update = function(dt) {
// Game programming stuff
}
بهروزرسانی
اگر میخواهید کاربران قادر باشند نوع سازنده را نیز گسترش دهند (برای سفارشیسازی قسمت «استاتیک» کلاس)، میتوانید این کار را با کمی کار اضافی انجام دهید. این شامل اضافه کردن یک پارامتر جنریک دیگر برای نوع سازنده سفارشی است. کاربران همچنین باید یک رابط ارائه دهند که آن نوع را توصیف کند - MyScriptClass در این مثال:
export interface ScriptConstructorArgs {
app: {}; // dummy empty type declarations here
entity: {};
}
export interface Script { // decsribes base class
app: {};
entity: {};
}
export interface ScriptConstructor<S extends Script> {
_name: string;
new(args: ScriptConstructorArgs): S;
}
// type declaration for createScript
declare var createScript: <Instance extends Script,
Class extends ScriptConstructor<Instance>
>(name: string) => Class;
interface MyScript extends Script {
update(dt: {}): void;
}
interface MyScriptClass extends ScriptConstructor<MyScript> {
someVar: number;
}
var MyScript = createScript<MyScript, MyScriptClass>('myScript');
MyScript.prototype.update = function(dt) {
// Game programming stuff
}
MyScript.someVar = 6;
فقط آگاه باشید که در این راهحل، کامپایلر واقعاً بررسی نمیکند که پیادهسازی ارائهشده با رابط اعلامشده مطابقت دارد یا خیر - شما میتوانید هر چیزی را به prototype.update اختصاص دهید و کامپایلر هیچ اعتراضی نخواهد کرد. همچنین، تا زمانی که prototype.update و someVar اختصاص داده نشوند، وقتی سعی میکنید از آنها استفاده کنید مقدار undefined دریافت میکنید و کامپایلر این مورد را نیز تشخیص نخواهد داد.
بهروزرسانی دیگر
دلیل اینکه انتساب به prototype.udpate بررسی نوع نمیشود این است که به نوعی بخش ایستا MyScript به عنوان Function استنتاج میشود، و Function.prototype در کتابخانهٔ داخلی به عنوان any اعلام شده است که بررسی نوع را سرکوب میکند. یک راه حل ساده وجود دارد: فقط prototype خود را با نوع اختصاصیتر اعلام کنید:
export interface ScriptConstructor<S extends Script> {
_name: string;
new(args: ScriptConstructorArgs): S;
prototype: S;
}
سپس شروع به گرفتن خطاهایی مثل این میکند:
MyScript.prototype.udpate = function(dt) {
// Game programming stuff
}
// Property 'udpate' does not exist on type 'MyScript'. Did you mean 'update'?
همچنین، نوع پارامتر dt نیز از رابط MyScript استنتاج میشود.
در واقع تجربه زیادی در انجام کارهایی مثل این ندارم، بنابراین نمیدانم اعلام کردن پروتوتایپ خودتان ممکن است در آینده باعث ایجاد مشکلاتی با کد دیگر شود یا نه.
همچنین، اگر انتساب به prototype.update اصلاً وجود نداشته باشد، شکایتی نخواهد کرد - هنوز هم باور خواهد داشت که update وجود دارد، زیرا در نوع createScript اعلام شده بود. این قیمت استفاده از "سرهمبندی" یک کلاس از پیادهسازی جاوااسکریپت همراه با برخی اعلامیههای نوع خارجی است - روش "عادی" و تمام تایپاسکریپتی برای ایجاد یک کلاس این است که از چیزی مانند class MyScript extends ScriptBase implements SomeInterface استفاده کنید، سپس تضمین میشود که بررسی نوع انجام شود.
