تایپ اسکریپت از نسخه ۲٫۸ به بعد پشتیبانی از تایپهای شرطی را معرفی کرده است. تایپهای شرطی افزودنی بسیار مفیدی هستند که به ما کمک میکنند تا بتوانیم کدی با قابلیت استفاده مجدد بنویسیم. در این مقاله قصد داریم تا این تایپها که بررسی کرده و بیشتر با آنها آشنا شویم.
تایپهای شرطی به ما اجازه میدهند که تبدیلهای تایپ را وابسته به یک شرط مخصوص تعریف کنیم. به طور خلاصه، آنها یک عملگر شرطی سه تایی هستند که به جای سطح مقدار در سطح تایپ اعمال میشوند.
تایپهای شرطی به صورت زیر تعریف میشوند:
type ConditionalType = SomeType extends OtherType ? TrueType : FalseType
توضیح کد بالا به زبان ساده به شکل زیر میباشد:
اگر یک تایپ معین SomeType
تایپ دیگرOtherType
را extend کند، در این صورت onditionalType
برابر TrueType
است، در غیر این صورت FalseType
میباشد.
منظور از تextends
در اینجا به این معنی است که هر مقداری از تایپ SomeType
جزء تایپ OtherType
نیز باشد.
تایپهای شرطی میتوانند بازگشتی باشند. یعنی این که یک و یا هر دو شاخهها میتواند خود یک تایپ شرطی باشد:
type Recursive<T> = T extends string[] ? string : (T extends number[] ? number : never) const a: Recursive<string[]> = "10" // works const b: Recursive<string> = 10 // Error: Type 'number' is not assignable to type 'never'.
یکی از مزایای اصلی تایپهای شرطی توانایی آنها در محدود کردن انواع تایپهای احتمالی یک تایپ عمومی است.
برای مثال، فرض کنید میخواهیم ExtractIdType<T>
را تعریف کنیم تا از یک T
عمومی، تایپ یک ویژگی بهخصوص به نام id
را استخراج کنیم. در این حالت، تایپ عمومی واقعی T
باید دارای ویژگی به نام id
باشد. در ابتدا، ممکن است به چیزی شبیه به قطعه کد زیر برسیم:
type ExtractIdType<T extends {id: string | number}> = T["id"] interface NumericId { id: number } interface StringId { id: string } interface BooleanId { id: boolean } type NumericIdType = ExtractIdType<NumericId> // type NumericIdType = number type StringIdType = ExtractIdType<StringId> // type StringIdType = string type BooleanIdType = ExtractIdType<BooleanId> // won't work
اینجا ما به صراحت بیان کردیم که T
باید دارای یک ویژگی به نام id
، با تایپ string
و یا number
باشد. سپس سه رابط تعریف کردیم که عبارتند از: NumericId
، StringId
و BooleanId
.
اگر بخواهیم تایپ ویژگی id
را استخراج کنیم، تایپ اسکریپت به درستی string
و number
را به ترتیب برای StringId
و NumericId
برمیگرداند.
اما این اتفاق برای BooleanId
ناموفق خواهد بود: Type 'BooleanId' does not satisfy the constraint '{ id: string | number; }'. Types of property 'id' are incompatible. Type 'boolean' is not assignable to type 'string | number'
اکنون سوالی که وجود دارد این است که چگونه میتوانیم ExtractIdType
خود را افزایش دهیم تا هر تایپ T
را بپذیرد و اگر T
ویژگی id
مورد نیاز را تعریف نکرده باشد چطور به چیزی شبیه به never
متوسل شویم؟ این کار را میتوانیم با استفاده از تایپهای شرطی انجام دهیم:
type ExtractIdType<T> = T extends {id: string | number} ? T["id"] : never interface NumericId { id: number } interface StringId { id: string } interface BooleanId { id: boolean } type NumericIdType = ExtractIdType<NumericId> // type NumericIdType = number type StringIdType = ExtractIdType<StringId> // type StringIdType = string type BooleanIdType = ExtractIdType<BooleanId> // type BooleanIdType = never
به سادگی و با جابجایی محدودیت در تایپ شرطی، توانستیم تعریف BooleanIdType
را به درستی انجام دهیم. در نسخه دوم کدی که داریم، تایپ اسکریپت میداند که اگر شاخه اول درست باشد، T
یک ویژگی به نام id
دارد که تایپ آن string | number
است.
استفاده از تایپهای شرطی برای اعمال محدودیتها و استخراج تایپ ویژگیها بسیار رایج است که میتوانیم از یک سینتکس ساده برای آن استفاده کنیم. برای مثال، میتوانیم تعریف ExtractIdType
را به صورت زیر بازنویسی کنیم:
type ExtractIdType<T> = T extends {id: infer U} ? T["id"] : never interface BooleanId { id: boolean } type BooleanIdType = ExtractIdType<BooleanId> // type BooleanIdType = boolean
در این مورد، تایپ ExtractIdType
را اصلاح کردیم. یعنی به جای اینکه ویژگی id
را مجبور کنیم که تایپ از نوع رشته و یا عدد داشته باشد، یک تایپ جدید U
با استفاده از کلمه کلیدی infer معرفی کردهایم. به همین دلیل BooleanIdType
دیگر ارزیابی نمیشود. در واقع همانطور که انتظار داریم تایپ اسکریپت boolean
را استخراج میکند.
infer
به جای اینکه مشخص کند چگونه تایپ المنتی را از شاخه واقعی بازیابی کنیم، راهی برای معرفی یک تایپ عمومی جدید در اختیار ما قرار میدهد.
در پایان این مقاله چند تایپ داخلی مفید با تکیه بر کلمه کلیدی infer
را بررسی خواهیم کرد.
در تایپ اسکریپت، تایپهای شرطی بر روی تایپهای union توزیع میشوند. به عبارت دیگر، هنگامی که ارزیابی در برابر یک تایپ union اتفاق میافتد، تایپ شرطی بر روی همه اعضای union اعمال میشود. به عنوان مثال:
type ToStringArray<T> = T extends string ? T[] : never type StringArray = ToStringArray<string | number>
در مثال بالا، ما به سادگی یک تایپ شرطی به نام ToStringArray
را تعریف کردیم. ارزیابی آن به string[]
اگر و تنها در صورتی انجام میشود که پارامتر عمومی string
باشد، در غیر این صورت never
ارزیابی میشود.
اکنون بررسی کنیم که تایپ اسکریپت برای تعریف StringArray
چگونه ToStringArray<string | number>
را ارزیابی میکند. ابتدا ToStringArray
را روی union توزیع میکند:
type StringArray = ToStringArray<string> | ToStringArray<number>
سپس میتوانیم ToStringArray
را با تعریف آن جایگزین کنیم:
type StringArray = (string extends string ? string[] : never) | (number extends string ? number[] : never)
ارزیابی تایپهای شرطی ما را با تعریف زیر روبرو میکند:
type StringArray = string[] | never
از آنجایی که never
یک تایپ فرعی نیست، میتوانیم آن را از union حذف کنیم:
type StringArray = string[]
اغلب اوقات ویژگی توزیعی تایپهای شرطی مورد نظر ما میباشد. با این وجود، برای جلوگیری از آن میتوانیم هر دو طرف کلمه کلیدی extends
را با براکت محصور کنیم
type ToStringArray<T> = [T] extends [string] ? T[] : never
در این شرایط هنگام ارزیابی StringArray
، تعریف ToStringArray
دیگر توزیع نمیشود:
type StringArray = ((string | number) extends string ? (string | number)[] : never)
از این رو، از آنجایی که string | number
extend نمیشود بنابراین string, StringArray
تبدیل به never
میشود.
در نهایت، اگر تایپ union بخشی از یک عبارت بزرگتر باشد (به عنوان مثال، یک تابع، یک آبجکت یا تاپل)، ویژگی توزیعی برقرار نمیشود. مهم نیست که این عبارت بزرگتر قبل یا بعد از extends
ظاهر شود. به عنوان مثال:
type NonDistributiveFunction<T> = (() => T) extends (() => string | number) ? T : never
type Fun1 = NonDistributiveFunction<string | boolean> // type Fun1 = never type Fun2 = NonDistributiveFunction<string> // type Fun2 = string
در این بخش چند نمونه از تایپهای شرطی تعریف شده توسط کتابخانه استاندارد تایپ اسکریپت را بررسی خواهیم کرد:
NonNullable<T>
NonNullable<T>
مقادیر null
و undefined
را از تایپ T
فیلتر میکند:
type NonNullable<T> = T extends null | undefined ? never : T type A = NonNullable<number> // number type B = NonNullable<number | null> // number type C = NonNullable<number | undefined> // number type D = NonNullable<null | undefined> // never
Extract<T, U> and Exclude<T, U>
Extract<T, U>
و Exclude<T, U>
مخالف یکدیگر هستند. اولی تایپ T
را فیلتر میکند تا همه تایپهایی که قابل انتساب به U
هستند را نگه دارد. اما دومی تایپهایی را که قابل انتساب به U
نیستند را حفظ میکند:
type Extract<T, U> = T extends U ? T : never type Exclude<T, U> = T extends U ? never : T type A = Extract<string | string[], any[]> // string[] type B = Exclude<string | string[], any[]> // string type C = Extract<number, boolean> // never type D = Exclude<number, boolean> // number
در مثال بالا هنگام تعریف A
، از تایپ اسکریپت خواستیم تا همه تایپهایی را که قابل انتساب به any[]
نیستند، از string | string[]
فیلتر کند. نتیجه این فقط رشته خواهد بود، زیرا string[]
کاملاً قابل انتساب به any[]
است. اما وقتی B
را تعریف کردیم، از تایپ اسکریپت خواستیم که دقیقا برعکس عمل کند. همانطور که انتظار میرود، نتیجه به جای string[]
یک رشته است.
همین آرگومان برای C
و D
نیز صادق است. در تعریف C
، عدد قابل انتساب به boolean
نیست. از این رو، تایپ اسکریپت never
را به عنوان یک تایپ استنتاج میکند. همینطور در تعریف D
، تایپ اسکریپت number
را نگه میدارد.
Parameters<T> and ReturnType<T>
Parameters<T>
و ReturnType<T>
به ترتیب به ما اجازه میدهند تا تمام تایپهای پارامتر و تایپ برگشتی یک تایپ تابع را استخراج کنیم:
type Parameters<T> = T extends (...args: infer P) => any ? P : never type ReturnType<T> = T extends (...args: any) => infer R ? R : any type A = Parameters<(n: number, s: string) => void> // [n: number, s: string] type B = ReturnType<(n: number, s: string) => void> // void type C = Parameters<() => () => void> // [] type D = ReturnType<() => () => void> // () => void type E = ReturnType<D> // void
تعریف Parameters<T>
کمی پیچیده است. اساساً یک تایپ تاپل با تمام تایپهای پارامتر (اگر T
یک تابع نباشد never
) تولید میکند.
به طور خاص، (...args: infer P) => any
یک تایپ تابع را نشان میدهد که در آن تایپ واقعی همه پارامترها (P
) استنتاج میشود. هر تابعی قابل تخصیص به این خواهد بود، زیرا هیچ محدودیتی در تایپ پارامترها وجود ندارد و نوع بازگشتیany
است.
به طور مشابه،ReturnType<T>
تایپ بازگشتی یک تابع را استخراج میکند. در این حالت از any
برای نشان دادن اینکه پارامترها میتوانند از هر تایپی باشند استفاده میکنیم. سپس تایپ بازگشتی R
را استنباط میکنیم.
ConstructorParameters<T> and InstanceType<T>
ConstructorParameters<T>
و InstanceType<T>
مشابه Parameters<T>
و ReturnType<T>
هستند با این تفاوت که به جای تایپ تابع، بر روی تایپهای تابع سازنده اعمال میشوند:
type ConstructorParameters<T> = T extends new (...args: infer P) => any ? P : never type InstanceType<T> = T extends new (...args: any[]) => infer R ? R : any interface PointConstructor { new (x: number, y: number): Point } class Point { private x: number; private y: number; constructor(x: number, y: number) { this.x = x; this.y = y } } type A = ConstructorParameters<PointConstructor> // [x: number, y: number] type B = InstanceType<PointConstructor> // Point
در این مقاله به بررسی تایپهای شرطی در تایپ اسکریپت پرداختیم. از تعریف اولیه و نحوه استفاده از آن برای اعمال محدودیتها شروع کردیم. سپس دیدیم که استنتاج تایپ چگونه کار میکند و عملکرد ویژگی توزیع تایپهای union را بررسی کردیم. در نهایت، با برخی از تایپهای شرطی رایج که توسط تایپ اسکریپت تعریف شدهاند آشنا شدیم، تعاریف آنها را تجزیه و تحلیل کرده و با چند مثال به شکل کامل بررسی نمودیم.
همانطور که در این مقاله دیدیم، تایپهای شرطی یکی از ویژگیهای بسیار پیشرفته سیستم تایپ هستند. با این حال، احتمالاً به صورت روزانه از آنها استفاده خواهیم کرد زیرا کتابخانه استاندارد تایپ اسکریپت به طور گسترده از تایپهای شرطی استفاده میکند.
۵۰ درصد تخفیف ویژه زمستان فرانت کست تا ۱۴ دی
کد تخفیف: wnt
دیدگاهها: