تعریف یک آرایه از انواع عمومی مختلف در تایپاسکریپت


interface Instruction {
  promise: Promise<unknown>,
  callback?: ($html: JQuery, data: unknown ) => void
}

const arr: Instruction[] = [
  { promise: Promise.resolve({ foo: 'bar' }), callback: ($html, data) => console.log(data.foo) },
  { promise: Promise.resolve({ bar: 'foo' }), callback: ($html, data) => console.log(data.bar) }
];

با توجه به موارد بالا، من میخواهم TypeScript تشخیص دهد که پارامتر data در تابع callback از همان نوعی است که Promise حل میشود.

اگر مستقل بود، میتوانستم انجام دهم:

interface Instruction<T> {
  promise: Promise<T>,
  callback?: ($html: JQuery, data: T) => void
}

اما پس چگونه میتوانم آرایه را تعریف کنم، جایی که T میتواند در هر خط معنای متفاوتی داشته باشد؟

پاسخ شماره ۱

این عملاً مورد استفاده کلاسیک برای نوعهای عمومی هستیشناسانه است، که به صورت مستقیم در تایپاسکریپت پشتیبانی نمیشوند (آنها به صورت مستقیم در اکثر زبانهای دارای نوعهای عمومی هم پشتیبانی نمیشوند، بنابراین این یک نقص خاص تایپاسکریپت نیست). یک درخواست ویژگی باز وجود دارد، microsoft/TypeScript#14466، که برای این موضوع است، اما تا نسخه TS4.1 بخشی از زبان نیست.

جنریکها در TypeScript «جهانی» هستند، به این معنا که وقتی میگویم class Foo<T> {...} منظورم این است که برای تمام پارامترهای نوع ممکن T کار میکند. این امکان را به مصرفکننده یک Foo<T> میدهد که مقدار T را مشخص کرده و با آن هرکاری که میخواهند انجام دهند، در حالی که ارائهدهنده Foo<T> باید اجازه دهد که تمام احتمالات در نظر گرفته شوند.

مجموعههای ناهمگن مثل همان موردی که شما تلاش میکنید توصیف کنید، به ژنریکهای «وجودی» نیاز دارند. به نوعی شما میخواهید interface Instruction<exists T> {...} به این معنا باشد که یک پارامتر نوع T وجود دارد که برای آن کار میکند. این به این معناست که ارائهدهنده یک Instruction میتواند مقدار T را مشخص کند و با آن هر کاری بخواهد انجام دهد، در حالی که مصرفکننده یک Instruction باید تمام امکانها را در نظر بگیرد.

برای اطلاعات بیشتر درباره جنریکهای کمّیشده بهصورت جهانی در مقابل وجودی، این سؤال و پاسخ در Stack Overflow را ببینید.


در حالی که پشتیبانی مستقیم از وجودگراییها (existentials) در تایپاسکریپت وجود ندارد، پشتیبانی غیرمستقیم وجود دارد. تفاوت بین یک جامعیت (universal) و وجودگرایی به این مربوط میشود که چه کسی به نوع نگاه میکند. اگر نقش تولیدکننده و مصرفکننده را عوض کنید، رفتار شبیه به وجودگرایی به دست میآید. این میتواند از طریق کالبکها تحقق یابد. بنابراین وجودگراییها میتوانند در تایپاسکریپت رمزگذاری شوند.

بیایید ببینیم چگونه ممکن است این کار را برای Instruction انجام دهیم. ابتدا، بیایید Instruction را به عنوان یک جنریک جهانی تعریف کنیم، نسخه «مستقل» که شما ذکر کردید (و من وابستگی JQuery را در این کد حذف میکنم):

interface Instruction<T> {
    promise: Promise<T>,
    callback?: (data: T) => void
}

در اینجا رمزگذاری وجودی، SomeInstruction است:

type SomeInstruction = <R>(cb: <T>(instruction: Instruction<T>) => R) => R;

یک SomeInstruction تابعی است که یک تابع را فراخوانی میکند که یک Instruction<T> را برای هر T میپذیرد و نتیجه را برمیگرداند. توجه کنید که چگونه SomeInstruction خود دیگر به T وابسته نیست. ممکن است از خود بپرسید چگونه میتوان یک SomeInstruction را دریافت کرد، اما این نیز نسبتاً ساده است. بیایید یک تابع کمکی بسازیم که هر Instruction<T> را به یک SomeInstruction تبدیل کند:

const someInstruction = <T,>(i: Instruction<T>): SomeInstruction => cb => cb(i);

سرانجام میتوانیم مجموعه ناهمگن شما را بسازیم:

const arr: SomeInstruction[] = [
    someInstruction({ 
      promise: Promise.resolve({ foo: 'bar' }), 
      callback: (data) => console.log(data.foo) 
    }),
    someInstruction({ 
      promise: Promise.resolve({ bar: 'foo' }), 
      callback: (data) => console.log(data.bar) 
    })
]

این همه نوع چکها، همانطور که خواسته شده است.


در واقع استفاده از یک SomeInstruction قدری پیچیدهتر از استفاده از یک Instruction<T> است، زیرا نیاز به یک callback دارد. اما وحشتناک نیست و باز هم اجازه میدهد پارامتر نوع T به گونهای ظاهر شود که مصرفکننده نداند نوع واقعی T چیست و بنابراین باید آن را به عنوان هر نوع ممکن از T در نظر بگیرد:

// writing out T for explicitness here
arr.forEach(someInstruction => someInstruction(<T,>(i: Instruction<T>) => {    
    i.promise.then(i.callback); // works
}))

// but it is not necessary:
arr.forEach(someInstruction => someInstruction(i => {    
    i.promise.then(i.callback); // works
}))

خوب.


راهحلهای جایگزین دیگری هم وجود دارد، اما چیزی که واقعاً میخواهید یک وجودی است. برای کامل بودن، در اینجا چند راهحل جایگزین ممکن وجود دارد که من به آنها اشاره میکنم اما آنها را پیادهسازی نمیکنم:

  • از ایمنی نوع صرف نظر کنید و از any یا unknown استفاده کنید و از تأیید نوع برای بازگرداندن نوعهایی که میخواهید استفاده کنید. بیمزه.

  • از یک نوع تاپل نگاشت شده استفاده کنید که نوعی مانند [T1, T2, T3] را به نوع متناظر [Instruction<T1>, Instruction<T2>, Instruction<T3>] تبدیل میکند. با این حال، این با push() کار نخواهد کرد، بنابراین باید به نحوی راه حلی برای آن پیدا کنید.

  • کد Instruction را بازسازی کنید تا نیازی به ژنریکها نداشته باشد و آنها را آشکار نکند. هر کاری که مصرفکننده قصد دارد با یک Instruction<T> انجام دهد باید مستقل از T باشد (مثلاً میتوانم بنویسم i.promise.then(i.callback) اما کار زیادی نمیتوان انجام داد، درست است؟)، بنابراین یک تابع Instruction ژنریک بسازید که برای ایجاد نیاز به یک جفت اعتبارسنجی promise و callback معتبر دارد و چیزی غیر ژنریک برگرداند با هر عملکردی که نیاز دارید و همین. به نوعی این یک «وجودی ساده» است که اجازه نمیدهد مصرفکننده به جفت promise-callback داخلی به صورت جداگانه دسترسی داشته باشد.


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