تایپاسکریپت: آیا راهی وجود دارد که اگر همه خصوصیات اختیاری باشند، نیاز به شیء نباشد؟
من روی یک کتابخانه (نوعی پروکسی API) کار میکنم که در پروژههای داخلی دیگر استفاده خواهد شد. میخواهم متدهایی تعریف کنم تا استفاده از آن آسان باشد، اما در مورد تایپها دچار مشکل هستم:
اول من یک کارخانه برای همه متدهای انتهایی دارم:
export type Params<TQueryParams> = {
customConfig?: object;
fullResponse?: boolean;
} & (TQueryParams extends undefined ? { queryParams?: undefined } : { queryParams: TQueryParams })
export const getFactory = <
TQueryParams extends object | undefined = undefined
>({ path }: any) => async (
{ queryParams, customConfig, fullResponse = false }: Params<TQueryParams>
): Promise<any> => {
// some irrelevant factory code
return {};
};
من قبلاً کمی جادو استفاده کردهام، پس بسته به TQueryParams ممکن است لازم باشد یا نباشد، اما این کافی نیست.
پس حالا وقتی میخواهم همه نقاط انتهایی را تعریف کنم و از کارخانه استفاده کنم:
// Case 1 - no interface
export const getFile = getFactory({
path: '/api/file'
});
// I'd like for this one to not throw error
getFile();
// These will not throw error - as intended
getFile({});
getFile({
fullResponse: true
});
// This will throw error - as intended
getFile({
queryParams: {}
});
// Case 2 - interface with all optional props
export interface ImageQueryParams {
width?: number;
height?: number;
}
export const getImage = getFactory<ImageQueryParams>({
path: '/api/image'
});
// All of them should be ok, but only last will not throw error
getImage();
getImage({});
getImage({
fullResponse: true
});
getImage({
queryParams: {}
});
// Case 3 - interface with mandatory props
export interface DataQueryParams {
id: number;
sort?: string;
}
export const getData = getFactory<DataQueryParams>({
path: '/api/data'
});
// All will throw error - as intended
getData();
getData({});
getData({
fullResponse: true
});
getData({
queryParams: {}
});
// This one will not throw error - as intended
getData({
queryParams: {
id: 2131241
}
});
فقط حالت ۳ همانطور که انتظار میرفت کار میکند.
من به ویژه میخواهم به راهحلی دست یابم که اگر رابطی وجود نداشت یا همه ویژگیها اختیاری بودند، بتوانم هیچ چیزی ارسال نکنم (getImage();) و مشکلی نباشد.
لینک TS Playground
پاسخ شماره ۱
من از این نوع تعریفها خیلی خوشحال نیستم (امیدوار بودم چیزی سادهتر و کلیتر باشد) اما به نظر میرسد که برای موارد استفاده شما کار میکنند:
type CanBeOmitted<T, Y = T, N = never> =
{} extends T ? Y : // T is weak (all props are optional), or
undefined extends T ? Y : // T can be undefined
N;
نوع مستعار CanBeOmitted<T, Y, N> بررسی میکند که آیا T یا یک نوع شیء همهاختیاری (که به آن «نوع ضعیف» نیز گفته میشود) است یا یک نوع سازگار با undefined. اگر چنین باشد، Y را برمیگرداند؛ در غیر این صورت، N (که به طور پیشفرض never است) را برمیگرداند. میتوانیم از این چند بار استفاده کنیم تا نوع Params خود را بسازیم.
این هم هست:
export type Params<TQueryParams> =
CanBeOmitted<TQueryParams, [undefined?]> |
[
{
customConfig?: object,
fullResponse?: boolean
} & (
{ queryParams: TQueryParams } |
CanBeOmitted<TQueryParams, { queryParams?: never }>
)
];
یک کاری که من متفاوت انجام دادهام این است که آن را به یک نوع تاپل حاوی پارامتر تبدیل کردهام. اگر پارامتر بتواند کاملاً حذف شود، یک تاپل اختیاری بازگردانده میشود.
اینجا getFactory است:
export const getFactory = <
TQueryParams extends object | undefined = undefined
>({ path }: any) => async (
...args: Params<TQueryParams>
): Promise<any> => {
// get rid of conditional types
const arg = args[0] as {
customConfig?: object,
fullResponse?: boolean,
queryParams?: TQueryParams
} | undefined;
const queryParams = arg ? arg.queryParams : undefined;
const customConfig = arg ? arg.customConfig : undefined;
const fullResponse = (arg ? arg.fullResponse : undefined) || false;
// some irrelevant factory code
return {};
};
توجه داشته باشید که Params<TQueryParams> بهعنوان یک تاپل باقیمانده استفاده میشود و نه بهعنوان یک پارامتر واحد. هنگامی که آن تاپل اختیاری باشد، این امکان را فراهم میکند که مقدار بازگشتی getFactory() بدون آرگومان فراخوانی شود. همچنین توجه داشته باشید که دیگر نمیتوانیم فرض کنیم که پارامتر وجود دارد، بنابراین باید نحوه دریافت queryParams، customConfig و fullResponse بهعنوان متغیر در درون پیادهسازی تغییر کند.
خب، بیایید ببینیم این چگونه کار میکند:
// Case 1 - no interface
export const getFile = getFactory({
path: '/api/file'
});
getFile(); // okay
getFile({}); // okay
getFile({
fullResponse: true
}); // okay
getFile({
queryParams: {}
}); // error, {} is not undefined
// Case 2 - interface with all optional props
export interface ImageQueryParams {
width?: number;
height?: number;
}
export const getImage = getFactory<ImageQueryParams>({
path: '/api/image'
});
getImage(); // okay
getImage({}); // okay
getImage({
fullResponse: true
}); // okay
getImage({
queryParams: {}
}); // okay
export interface DataQueryParams {
id: number;
sort?: string;
}
// Case 3 - interface with mandatory props
export const getData = getFactory<DataQueryParams>({
path: '/api/data'
});
getData(); // error, expected 1 arg
getData({}); // error, queryParams missing
getData({
fullResponse: true
}); // error, queryParams missing
getData({
queryParams: {}
}); // error, id missing
getData({
queryParams: {
id: 2131241
}
}); // okay
من فکر میکنم این همان چیزی است که دنبال آن میگردید. خوب، امیدوارم که به شما کمک کند. موفق باشید!
لینک به کد
