آنچه که ممکن است در تایپ اسکریپت به عنوان Generic درنظر بگیریم در واقع سه مفهوم مجزا است که عبارتند از:
// ۱٫ Passing types to types type PartialUser = Partial<{ id: string; name: string }>; // ۲٫ Passing types to functions const stringSet = new Set<string>(); // ۳٫ Inferring types from arguments passed to functions const objKeys = <T extends object>(obj: T): Array<keyof T> => { return Object.keys(obj) as Array<keyof T>; }; const keys = objKeys({ a: 1, b: 2 }); // ^? const keys: ("a" | "b")[]
با انتقال تایپ به تایپ شروع میکنیم.
در تایپ اسکریپت، ما میتوانیم تایپی را تعریف کنیم که نشاندهنده یک آبجکت، primitive، تابع یا هر چیز دیگری که میخواهیم، باشد.
export type User = { id: string; name: string; }; export type ToString = (input: any) => string;
اما فرض کنید باید چند تایپ با ساختار مشابه ایجاد کنیم. به عنوان مثال، یک data shape.
مثال زیر را در نظر بگیرید:
type ErrorShape = { message: string; code: number; }; type GetUserData = { data: { id: string; name: string; }; error?: ErrorShape; }; type GetPostsData = { data: { id: string; title: string; }; error?: ErrorShape; }; type GetCommentsData = { data: { id: string; content: string; }; error?: ErrorShape; };
اگر مایل باشیم که از OOP استفاده کنیم، میتوانیم این کار را با استفاده از یک interface با قابلیت استفاده مجدد مانند کد زیر انجام دهیم:
interface DataBaseInterface { error?: ErrorShape; } interface GetUserData extends DataBaseInterface { data: { id: string; name: string; }; } interface GetPostsData extends DataBaseInterface { data: { id: string; title: string; }; } interface GetCommentsData extends DataBaseInterface { data: { id: string; content: string; }; }
اما ایجاد یک type function مختصرتر است، که تایپ داده را دریافت کرده و data shape جدید را برمیگرداند:
// Our new type function! type DataShape<TData> = { data: TData; error?: ErrorShape; }; type GetUserData = DataShape<{ id: string; name: string; }>; type GetPostsData = DataShape<{ id: string; title: string; }>; type GetCommentsData = DataShape<{ id: string; content: string; }>;
درک این سینتکس مهم است، زیرا در ادامه مقاله با آن سروکار خواهیم داشت.
براکتهای موجود در <TData>
به تایپ اسکریپت میگویند که ما میخواهیم یک تایپ آرگومان به این تایپ اضافه کنیم. ما میتوانیم TData
را هر چیزی نامگذاری کنیم، این دقیقاً مانند آرگومان یک تابع میباشد.
مثالی که در ادامه داریم یک تایپ generic است:
// Generic type type DataShape<TData> = { data: TData; error?: ErrorShape; }; // Passing our generic type // another type type GetPostsData = DataShape<{ id: string; title: string; }>;
تایپهای generic میتوانند چندین نوع آرگومان را بپذیرند. ما میتوانیم پیشفرضها و همچنین محدودیتهایی را برای تایپ آرگومانها تنظیم کنیم تا فقط بتوانیم انواع خاصی را به آنها ارسال کنیم.
اما فقط تایپها نیستند که میتوانیم تایپ را به آنها منتقل کنیم:
const createSet = <T>() => { return new Set<T>(); }; const stringSet = createSet<string>(); // ^? const stringSet: Set<string> const numberSet = createSet<number>(); // ^? const numberSet: Set<number>
صحیح است، درست مانند تایپها که میتوانند تایپهای دیگر را به عنوان آرگومان بپذیرند، توابع نیز میتوانند این کار را انجام دهند.
در این مثال، زمانی که createSet را تعریف میکنیم، یک <T>
را به قبل از پرانتز اضافه مینماییم. سپس آن <T>
را به صورت دستی به Set()
انتقال میدهیم، که به ما اجازه میدهد تا تایپ آرگومان را ارسال کنیم:
export const createSet = <T>() => { return new Set<T>(); }
این بدان معناست که وقتی آن را فراخوانی میکنیم، میتوانیم یک تایپ آرگومان <string>
را به createSet
ارسال کنیم.
اکنون مجموعهای را بررسی میکنیم که فقط میتوانیم رشتهها را به آن منتقل کنیم:
const stringSet = createSet<string>(); // Argument of type 'number' is not assignable to parameter of type 'string'. stringSet.add(123);
این موضوع دومین چیزی است که هنگام صحبت در مورد genericها به آن اشاره میشود: انتقال دستی تایپها به توابع.
اگر از React استفاده کنیم این را خواهیم دید، و ما باید یک تایپ آرگومان را به useState
ارسال کنیم:
const [index, setIndex] = useState<number>(0); // ^? const index: number
اما متوجه رفتار دیگری در React میشویم که در برخی موارد، برای استنباط آن نیازی به ارسال تایپ آرگومان نداریم:
const [index, setIndex] = useState(0); // ^? const index: number // WHAT IS THIS SORCERY
برای اینکه ببینیم چه اتفاقی افتاده است، بار دیگر تابع createSet
را بررسی میکنیم. باید به این نکته توجه داشته باشیم که فقط تایپ آرگومانها را دریافت میکند:
export const createSet = <T>() => { return new Set<T>(); }
اگر تابع خود را طوری تغییر دهیم که یک مقدار اولیه برای مجموعه بپذیرد، در این صورت چه اتفاقی میافتد؟
میدانیم که مقدار اولیه باید از نوع Set باشد، بنابراین آن را به صورت T
مینویسیم:
export const createSet = <T>(initial: T) => { return new Set<T>([initial]); }
اکنون وقتی آن را فراخوانی میکنیم، باید یک initial
ارسال کنیم، و این باید دارای همان نوع تایپ آرگومانی باشد که به createSet
میفرستیم:
const stringSet = createSet<string>("matt"); // ^? const stringSet: Set<string> // Argument of type 'string' is not assignable to parameter of type 'number'. const numberSet = createSet<number>("pocock");
این اتفاق عجیبی نیست زیرا تایپ اسکریپت میتواند تایپ T
را از initial
استنباط کند. به عبارت دیگر، تایپ آرگومان از آرگومان زمان اجرا استنباط میشود.
const stringSet = createSet("matt"); // ^? const stringSet: Set<string> const numberSet = createSet(123); // ^? const numberSet: Set<number>
میتوانیم این موضوع را با نگه داشتن ماوس روی یکی از فراخوانیهای createSet
بررسی کنیم. وقتی رشتهای را به آن ارسال میکنیم، خواهیم دید که <string>
استنباط میشود:
// hovering over createSet const createSet: <string>(initial: string) => Set<string>
در useState
نیز همین امر صادق است، برای آن باید به سینتکس useState<number>
در tooltip مراجعه کنیم:
// hovering over useState useState<number>(initialState: number | (() => number)): [number, Dispatch<SetStateAction<number>>]
این موضوع همچنین برای تابع objKeys
که شامل برخی مزایای بیشتر میباشد نیز صادق است:
T
را به یک آبجکت محدود میکنیم تا بتوانیم آن را به Object.keys
(که فقط آبجکتها را میپذیرد) ارسال کنیم.Object.keys
از نوع Array<keyof T>
باشد.const objKeys = <T extends object>(obj: T) => { return Object.keys(obj) as Array<keyof T>; };
در این مقاله سعی کردیم تا با مفهوم Generic در تایپ اسکریپت بیشتر آشنا شویم. به طور خلاصه، آنچه که ما به عنوان generic فکر میکنیم در واقع سه الگوی مختلف است که عبارتند از:
DataShape<T>
createSet<string>()
useState(0)