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

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

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

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

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

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
type ConditionalType = SomeType extends OtherType ? TrueType : FalseType
type ConditionalType = SomeType extends OtherType ? TrueType : FalseType
type ConditionalType = SomeType extends OtherType ? TrueType : FalseType

توضیح کد بالا به زبان ساده به شکل زیر می‌باشد:

اگر یک تایپ معین

SomeType
SomeTypeتایپ دیگر
OtherType
OtherTypeرا extend کند، در این صورت
onditionalType
onditionalTypeبرابر
TrueType
TrueTypeاست، در غیر این صورت
FalseType
FalseTypeمی‌باشد.

منظور از ت

extends
extendsدر اینجا به این معنی است که هر مقداری از تایپ
SomeType
SomeTypeجزء تایپ
OtherType
OtherTypeنیز باشد.

تایپ‌های شرطی می‌توانند بازگشتی باشند. یعنی این که یک و یا هر دو شاخه‌ها می‌تواند خود یک تایپ شرطی باشد:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
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'.
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'.
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>
ExtractIdType<T>را تعریف کنیم تا از یک
T
Tعمومی، تایپ یک ویژگی به‌خصوص به نام
id
idرا استخراج کنیم. در این حالت، تایپ عمومی واقعی
T
T باید دارای ویژگی به نام
id
id باشد. در ابتدا، ممکن است به چیزی شبیه به قطعه کد زیر برسیم:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
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
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
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
Tباید دارای یک ویژگی به نام
id
id، با تایپ
string
stringو یا
number
numberباشد. سپس سه رابط تعریف کردیم که عبارتند از:
NumericId
NumericId،
StringId
StringIdو
BooleanId
BooleanId.

اگر بخواهیم تایپ ویژگی

id
idرا استخراج کنیم، تایپ اسکریپت به درستی
string
stringو
number
numberرا به ترتیب برای
StringId
StringIdو
NumericId
NumericIdبرمی‌گرداند.

اما این اتفاق برای

BooleanId
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'
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
ExtractIdTypeخود را افزایش دهیم تا هر تایپ
T
Tرا بپذیرد و اگر
T
T ویژگی
id
idمورد نیاز را تعریف نکرده باشد چطور به چیزی شبیه به
never
neverمتوسل شویم؟ این کار را می‌توانیم با استفاده از تایپ‌های شرطی انجام دهیم:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
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
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
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
BooleanIdTypeرا به درستی انجام دهیم. در نسخه دوم کدی که داریم، تایپ اسکریپت می‌داند که اگر شاخه اول درست باشد،
T
Tیک ویژگی به نام
id
idدارد که تایپ آن
string | number
string | numberاست.

استنتاج تایپ در تایپ‌های شرطی

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

ExtractIdType
ExtractIdTypeرا به صورت زیر بازنویسی کنیم:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
type ExtractIdType<T> = T extends {id: infer U} ? T["id"] : never
interface BooleanId {
id: boolean
}
type BooleanIdType = ExtractIdType<BooleanId> // type BooleanIdType = boolean
type ExtractIdType<T> = T extends {id: infer U} ? T["id"] : never interface BooleanId { id: boolean } type BooleanIdType = ExtractIdType<BooleanId> // type BooleanIdType = boolean
type ExtractIdType<T> = T extends {id: infer U} ? T["id"] : never

interface BooleanId {
    id: boolean
}

type BooleanIdType = ExtractIdType<BooleanId> // type BooleanIdType = boolean

در این مورد، تایپ

ExtractIdType
ExtractIdTypeرا اصلاح کردیم. یعنی به جای اینکه ویژگی
id
idرا مجبور کنیم که تایپ از نوع رشته و یا عدد داشته باشد، یک تایپ جدید
U
Uبا استفاده از کلمه کلیدی infer معرفی کرده‌ایم. به همین دلیل
BooleanIdType
BooleanIdTypeدیگر ارزیابی نمی‌شود. در واقع همانطور که انتظار داریم تایپ اسکریپت
boolean
booleanرا استخراج می‌کند.

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

در پایان این مقاله چند تایپ داخلی مفید با تکیه بر کلمه کلیدی

infer
infer را بررسی خواهیم کرد.

تایپ‌های شرطی توزیعی

در تایپ اسکریپت، تایپ‌های شرطی بر روی تایپ‌های union توزیع می‌شوند. به عبارت دیگر، هنگامی که ارزیابی در برابر یک تایپ union اتفاق می‌افتد، تایپ شرطی بر روی همه اعضای union اعمال می‌شود. به عنوان مثال:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
type ToStringArray<T> = T extends string ? T[] : never
type StringArray = ToStringArray<string | number>
type ToStringArray<T> = T extends string ? T[] : never type StringArray = ToStringArray<string | number>
type ToStringArray<T> = T extends string ? T[] : never

type StringArray = ToStringArray<string | number>

در مثال بالا، ما به سادگی یک تایپ شرطی به نام

ToStringArray
ToStringArrayرا تعریف کردیم. ارزیابی آن به
string[]
string[]اگر و تنها در صورتی انجام می‌شود که پارامتر عمومی
string
stringباشد، در غیر این صورت
never
neverارزیابی می‌شود.

اکنون بررسی کنیم که تایپ اسکریپت برای تعریف

StringArray
StringArrayچگونه
ToStringArray<string | number>
ToStringArray<string | number>را ارزیابی می‌کند. ابتدا
ToStringArray
ToStringArrayرا روی union توزیع می‌کند:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
type StringArray = ToStringArray<string> | ToStringArray<number>
type StringArray = ToStringArray<string> | ToStringArray<number>
type StringArray = ToStringArray<string> | ToStringArray<number>

سپس می‌توانیم

ToStringArray
ToStringArrayرا با تعریف آن جایگزین کنیم:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
type StringArray = (string extends string ? string[] : never) | (number extends string ? number[] : never)
type StringArray = (string extends string ? string[] : never) | (number extends string ? number[] : never)
type StringArray = (string extends string ? string[] : never) | (number extends string ? number[] : never)

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

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
type StringArray = string[] | never
type StringArray = string[] | never
type StringArray = string[] | never

از آنجایی که

never
neverیک تایپ فرعی نیست، می‌توانیم آن را از union حذف کنیم:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
type StringArray = string[]
type StringArray = string[]
type StringArray = string[]

اغلب اوقات ویژگی توزیعی تایپ‌های شرطی مورد نظر ما می‌باشد. با این وجود، برای جلوگیری از آن می‌توانیم هر دو طرف کلمه کلیدی

extends
extendsرا با براکت محصور کنیم

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
type ToStringArray<T> = [T] extends [string] ? T[] : never
type ToStringArray<T> = [T] extends [string] ? T[] : never
type ToStringArray<T> = [T] extends [string] ? T[] : never

در این شرایط هنگام ارزیابی

StringArray
StringArray، تعریف
ToStringArray
ToStringArrayدیگر توزیع نمی‌شود:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
type StringArray = ((string | number) extends string ? (string | number)[] : never)
type StringArray = ((string | number) extends string ? (string | number)[] : never)
type StringArray = ((string | number) extends string ? (string | number)[] : never)

از این رو، از آنجایی که

string | number
string | numberextend نمی‌شود بنابراین
string, StringArray
string, StringArrayتبدیل به
never
neverمی‌شود.

در نهایت، اگر تایپ union بخشی از یک عبارت بزرگ‌تر باشد (به عنوان مثال، یک تابع، یک آبجکت یا تاپل)، ویژگی توزیعی برقرار نمی‌شود. مهم نیست که این عبارت بزرگ‌تر قبل یا بعد از

extends
extendsظاهر شود. به عنوان مثال:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
type NonDistributiveFunction<T> = (() => T) extends (() => string | number) ? T : never
type NonDistributiveFunction<T> = (() => T) extends (() => string | number) ? T : never
type NonDistributiveFunction<T> = (() => T) extends (() => string | number) ? T : never
Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
type Fun1 = NonDistributiveFunction<string | boolean> // type Fun1 = never
type Fun2 = NonDistributiveFunction<string> // type Fun2 = string
type Fun1 = NonDistributiveFunction<string | boolean> // type Fun1 = never type Fun2 = NonDistributiveFunction<string> // type Fun2 = string
type Fun1 = NonDistributiveFunction<string | boolean> // type Fun1 = never

type Fun2 = NonDistributiveFunction<string> // type Fun2 = string

تایپ‌های شرطی داخلی

در این بخش چند نمونه از تایپ‌های شرطی تعریف شده توسط کتابخانه استاندارد تایپ اسکریپت را بررسی خواهیم کرد:

NonNullable<T>

NonNullable<T>
NonNullable<T>مقادیر
null
nullو
undefined
undefinedرا از تایپ
T
Tفیلتر می‌کند:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
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
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
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>
Extract<T, U>و
Exclude<T, U>
Exclude<T, U>مخالف یک‌دیگر هستند. اولی تایپ
T
Tرا فیلتر می‌کند تا همه تایپ‌هایی که قابل انتساب به
U
Uهستند را نگه دارد. اما دومی تایپ‌هایی را که قابل انتساب به
U
U نیستند را حفظ می‌کند:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
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
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
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
A، از تایپ اسکریپت خواستیم تا همه تایپ‌هایی را که قابل انتساب به
any[]
any[]نیستند، از
string | string[]
string | string[]فیلتر کند. نتیجه این فقط رشته خواهد بود، زیرا
string[]
string[]کاملاً قابل انتساب به
any[]
any[] است. اما وقتی
B
Bرا تعریف کردیم، از تایپ اسکریپت خواستیم که دقیقا برعکس عمل کند. همانطور که انتظار می‌رود، نتیجه به جای
string[]
string[]یک رشته است.

همین آرگومان برای

C
Cو
D
Dنیز صادق است. در تعریف
C
C، عدد قابل انتساب به
boolean
booleanنیست. از این رو، تایپ اسکریپت
never
neverرا به عنوان یک تایپ استنتاج می‌کند. همینطور در تعریف
D
D، تایپ اسکریپت
number
numberرا نگه می‌دارد.

Parameters<T> and ReturnType<T>

Parameters<T>
Parameters<T>و
ReturnType<T>
ReturnType<T>به ترتیب به ما اجازه می‌دهند تا تمام تایپ‌های پارامتر و تایپ برگشتی یک تایپ تابع را استخراج کنیم:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
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
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
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>
Parameters<T>کمی پیچیده است. اساساً یک تایپ تاپل با تمام تایپ‌های پارامتر (اگر
T
Tیک تابع نباشد
never
never) تولید می‌کند.

به طور خاص،

(...args: infer P) => any
(...args: infer P) => anyیک تایپ تابع را نشان می‌دهد که در آن تایپ واقعی همه پارامترها (
P
P) استنتاج می‌شود. هر تابعی قابل تخصیص به این خواهد بود، زیرا هیچ محدودیتی در تایپ پارامترها وجود ندارد و نوع بازگشتی
any
anyاست.

به طور مشابه،

ReturnType<T>
ReturnType<T> تایپ بازگشتی یک تابع را استخراج می‌کند. در این حالت از
any
any برای نشان دادن اینکه پارامترها می‌توانند از هر تایپی باشند استفاده می‌کنیم. سپس تایپ بازگشتی
R
Rرا استنباط می‌کنیم.

ConstructorParameters<T> and InstanceType<T>

ConstructorParameters<T>
ConstructorParameters<T>و
InstanceType<T>
InstanceType<T>مشابه
Parameters<T>
Parameters<T>و
ReturnType<T>
ReturnType<T>هستند با این تفاوت که به جای تایپ تابع، بر روی تایپ‌های تابع سازنده اعمال می‌شوند:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
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
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
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 را بررسی کردیم. در نهایت، با برخی از تایپ‌های شرطی رایج که توسط تایپ اسکریپت تعریف شده‌اند آشنا شدیم، تعاریف آن‌ها را تجزیه و تحلیل کرده و با چند مثال به شکل کامل بررسی نمودیم.

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

دیدگاه‌ها:

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