تایپ شرطی در تایپ اسکریپت

تایپ اسکریپت از نسخه ۲٫۸ به بعد پشتیبانی از تایپ‌های شرطی را معرفی کرده است. تایپ‌های شرطی افزودنی بسیار مفیدی هستند که به ما کمک می‌کنند تا بتوانیم کدی با قابلیت استفاده مجدد بنویسیم. در این مقاله قصد داریم تا این تایپ‌ها که بررسی کرده و بیشتر با آن‌ها آشنا شویم.

تایپ شرطی چیست؟

تایپ‌های شرطی به ما اجازه می‌دهند که تبدیل‌های تایپ را وابسته به یک شرط مخصوص تعریف کنیم. به طور خلاصه، آن‌ها یک عملگر شرطی سه تایی هستند که به جای سطح مقدار در سطح تایپ اعمال می‌شوند.

تایپ‌های شرطی به صورت زیر تعریف می‌شوند:

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 | numberextend نمی‌شود بنابراین 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 را بررسی کردیم. در نهایت، با برخی از تایپ‌های شرطی رایج که توسط تایپ اسکریپت تعریف شده‌اند آشنا شدیم، تعاریف آن‌ها را تجزیه و تحلیل کرده و با چند مثال به شکل کامل بررسی نمودیم.

همانطور که در این مقاله دیدیم، تایپ‌های شرطی یکی از ویژگی‌های بسیار پیشرفته سیستم تایپ هستند. با این حال، احتمالاً به صورت روزانه از آن‌ها استفاده خواهیم کرد زیرا کتابخانه استاندارد تایپ اسکریپت به طور گسترده از تایپ‌های شرطی استفاده می‌کند.

دیدگاه‌ها:

افزودن دیدگاه جدید