TypeScript با خطاهای Go/Rust؟ امتحان / گرفتن؟ ارتداد.

بنابراین، اجازه دهید با کمی پیشینه درباره من شروع کنیم. من یک توسعه دهنده نرم افزار با حدود 10 سال تجربه هستم، در ابتدا با PHP کار کردم و سپس به تدریج به جاوا اسکریپت منتقل شدم. همچنین، این اولین مقاله من است، پس لطفا درک کنید 🙂
من حدود 5 سال پیش استفاده از TypeScript را شروع کردم و از آن زمان، هرگز به جاوا اسکریپت برنگشتم. لحظه ای که شروع به استفاده از آن کردم، فکر کردم که بهترین زبان برنامه نویسی است که تا کنون ساخته شده است. همه آن را دوست دارند، همه از آن استفاده می کنند … فقط بهترین است، درست است؟ درست؟ درست؟
بله، و سپس شروع کردم به بازی کردن با زبان های دیگر، زبان های مدرن تر. اول Go بود و بعد به آرامی Rust را به لیستم اضافه کردم (با تشکر از Prime).
وقتی نمیدانی چیزهای مختلف وجود دارند، از دست دادن چیزها سخت است.
من در مورد چه چیزی صحبت می کنم؟ Go and Rust چه چیزی مشترک دارد؟ خطاها. چیزی که بیشتر از همه برای من برجسته بود. و به طور خاص تر، نحوه برخورد این زبان ها با آنها.
جاوا اسکریپت برای رسیدگی به خطاها متکی به استثنائات است، در حالی که Go و Rust آنها را به عنوان مقادیر در نظر می گیرند. ممکن است فکر کنید که این مسئله چندان مهمی نیست… اما، پسر، ممکن است یک چیز بی اهمیت به نظر برسد. با این حال، این یک تغییر دهنده بازی است.
بیایید از میان آنها عبور کنیم. ما عمیقاً در هر زبان فرو نخواهیم رفت. ما فقط می خواهیم رویکرد کلی را بدانیم.
بیایید با جاوا اسکریپت / TypeScript و یک بازی کوچک شروع کنیم.
5 ثانیه به خود فرصت دهید تا به کد زیر نگاه کنید و پاسخ دهید چرا باید آن را در تلاش / گرفتن بپیچیم.
try {
const request = { name: "test", value: 2n };
const body = JSON.stringify(request);
const response = await fetch("https://example.com", {
method: "POST",
body,
});
if (!response.ok) {
return;
}
// handle response
} catch (e) {
// handle error
return;
}
بنابراین، من فرض میکنم که بیشتر شما حدس زدهاید که حتی اگر ما در حال بررسی آن هستیم response.ok
، روش واکشی همچنان می تواند خطا ایجاد کند. این response.ok
فقط خطاهای شبکه 4xx و 5xx را “گرفت”. اما وقتی خود شبکه از کار بیفتد، خطا می دهد.
اما من تعجب می کنم که چند نفر از شما این را حدس زدید JSON.stringify
همچنین خطا خواهد کرد. دلیل آن این است که شی درخواست حاوی عبارت است bigint (2n)
متغیر، که JSON
نمی داند چگونه رشته کند.
بنابراین اولین مشکل این است و شخصاً معتقدم این بزرگترین مشکل جاوا اسکریپت است: ما نمی دانیم چه چیزی می تواند خطا ایجاد کند. از منظر خطای جاوا اسکریپت، این یکسان است:
try {
let data = "Hello";
} catch (err) {
console.error(err);
}
جاوا اسکریپت نمی داند، جاوا اسکریپت اهمیتی نمی دهد. باید بدانی که.
نکته دوم، این یک کد کاملاً قابل اجرا است:
const request = { name: "test", value: 2n };
const body = JSON.stringify(request);
const response = await fetch("https://example.com", {
method: "POST",
body,
});
if (!response.ok) {
return;
}
بدون خطایی، بدون هیچ خطی، حتی اگر این می تواند برنامه شما را خراب کند.
در حال حاضر در ذهنم، می توانم بشنوم: “مشکل چیست، فقط سعی کنید / همه جا را بگیرید.” در اینجا مشکل سوم مطرح می شود. ما نمی دانیم کدام یک را پرتاب می کنیم. البته، ما می توانیم به نوعی با پیام خطا حدس بزنیم، اما برای خدمات / عملکردهای بزرگتر، با مکان های زیادی که ممکن است خطا رخ دهد؟ آیا مطمئن هستید که با یک بار امتحان / گرفتن همه آنها را به درستی مدیریت می کنید؟
خوب، وقت آن است که از انتخاب JS خودداری کنید و به سراغ چیز دیگری بروید. بیایید با Go شروع کنیم:
f, err := os.Open("filename.ext")
if err != nil {
log.Fatal(err)
}
// do something with the open *File f
ما در حال تلاش برای باز کردن یک فایل هستیم که یا یک فایل یا یک خطا را برمی گرداند. و شما این را زیاد خواهید دید، بیشتر به این دلیل که ما می دانیم که کدام توابع همیشه خطاها را برمی گردانند. شما هرگز یکی را از دست نمی دهید. در اینجا اولین مثال از برخورد با خطا به عنوان یک مقدار است. شما مشخص می کنید که کدام تابع می تواند آنها را برگرداند، آنها را برمی گردانید، آنها را اختصاص می دهید، آنها را بررسی می کنید، با آنها کار می کنید.
همچنین چندان رنگارنگ نیست، و همچنین یکی از مواردی است که Go به خاطر آن مورد انتقاد قرار می گیرد error-checking code
، جایی که if err != nil { ....
گاهی اوقات خطوط کد بیشتری نسبت به بقیه می گیرد.
if err != nil {
...
if err != nil {
...
if err != nil {
...
}
}
}
if err != nil {
...
}
...
if err != nil {
...
}
هنوز کاملا ارزش تلاش را دارد، به من اعتماد کنید.
و در نهایت Rust:
let greeting_file_result = File::open("hello.txt");
let greeting_file = match greeting_file_result {
Ok(file) => file,
Err(error) => panic!("Problem opening the file: {:?}", error),
};
پرمخاطب ترین سه موردی که در اینجا نشان داده شده و از قضا بهترین آنهاست. بنابراین اول از همه، Rust با استفاده از Enums شگفتانگیز خود، خطاها را کنترل میکند (آنها با enums TypeScript یکسان نیستند!). بدون پرداختن به جزئیات، آنچه در اینجا مهم است این است که از Enum به نام استفاده می کند Result
با دو نوع: Ok
و Err
. همانطور که ممکن است حدس بزنید، Ok
دارای ارزش و Err
جای تعجب دارد، یک خطا :D.
همچنین راه های زیادی برای مقابله با آنها به روش های راحت تر، برای کاهش مشکل Go دارد. شناخته شده ترین آنها است ?
اپراتور.
let greeting_file_result = File::open("hello.txt")?;
خلاصه اینجا این است که هم Go و هم Rust می دانند که هر جا ممکن است خطایی وجود داشته باشد، همیشه. و آنها شما را مجبور می کنند که دقیقاً در همان جایی که ظاهر می شود (بیشتر) با آن مقابله کنید. بدون آنهایی که پنهان، بدون حدس زدن، بدون برنامه شکستن با چهره شگفت انگیز.
و این رویکرد فقط بهتر است. با یک مایل.
خوب، وقت آن است که صادق باشم، کمی دروغ گفتم. ما نمیتوانیم خطاهای TypeScript مانند خطاهای Go/Rust عمل کنند. عامل محدود کننده در اینجا خود زبان است. فقط ابزار مناسبی برای این کار ندارد.
اما کاری که ما می توانیم انجام دهیم این است که سعی کنیم آن را مشابه کنیم. و آن را ساده کنید.
با این شروع کنید:
export type Safe<T> =
| {
success: true;
data: T;
}
| {
success: false;
error: string;
};
هیچ چیز در اینجا واقعاً جالب نیست، فقط یک نوع عمومی ساده است. اما این بچه کوچولو کاملاً می تواند کد را تغییر دهد. همانطور که ممکن است متوجه شوید، بزرگترین تفاوت در اینجا این است که ما یا داده ها را برمی گردانیم یا خطا داریم. آشنا به نظر می رسد؟
همچنین، دروغ دوم، ما نیاز به تلاش / گرفتن داریم. نکته خوب این است که ما فقط به حداقل دو مورد نیاز داریم، نه 100000.
export function safe<T>(promise: Promise<T>, err?: string): Promise<Safe<T>>;
export function safe<T>(func: () => T, err?: string): Safe<T>;
export function safe<T>(
promiseOrFunc: Promise<T> | (() => T),
err?: string,
): Promise<Safe<T>> | Safe<T> {
if (promiseOrFunc instanceof Promise) {
return safeAsync(promiseOrFunc, err);
}
return safeSync(promiseOrFunc, err);
}
async function safeAsync<T>(
promise: Promise<T>,
err?: string
): Promise<Safe<T>> {
try {
const data = await promise;
return { data, success: true };
} catch (e) {
console.error(e);
if (err !== undefined) {
return { success: false, error: err };
}
if (e instanceof Error) {
return { success: false, error: e.message };
}
return { success: false, error: "Something went wrong" };
}
}
function safeSync<T>(
func: () => T,
err?: string
): Safe<T> {
try {
const data = func();
return { data, success: true };
} catch (e) {
console.error(e);
if (err !== undefined) {
return { success: false, error: err };
}
if (e instanceof Error) {
return { success: false, error: e.message };
}
return { success: false, error: "Something went wrong" };
}
}
“وای، چه نابغه ای، او یک لفاف برای امتحان / گرفتن ایجاد کرد.” بله حق با شماست؛ این فقط یک لفاف با ما است Safe
به عنوان بازگشتی تایپ کنید. اما گاهی اوقات چیزهای ساده تنها چیزی است که شما نیاز دارید. بیایید آنها را با مثال بالا ترکیب کنیم.
قدیمی (16 خط):
try {
const request = { name: "test", value: 2n };
const body = JSON.stringify(request);
const response = await fetch("https://example.com", {
method: "POST",
body,
});
if (!response.ok) {
// handle network error
return;
}
// handle response
} catch (e) {
// handle error
return;
}
جدید (20 خط):
const request = { name: "test", value: 2n };
const body = safe(
() => JSON.stringify(request),
"Failed to serialize request",
);
if (!body.success) {
// handle error (body.error)
return;
}
const response = await safe(
fetch("https://example.com", {
method: "POST",
body: body.data,
}),
);
if (!response.success) {
// handle error (response.error)
return;
}
if (!response.data.ok) {
// handle network error
return;
}
// handle response (body.data)
بنابراین بله، راه حل جدید ما طولانی تر است، اما:
- بدون تلاش برای گرفتن
- ما هر خطا را در جایی که رخ می دهد مدیریت می کنیم
- ما می توانیم یک پیام خطا برای یک تابع خاص مشخص کنیم
- ما یک منطق خوب از بالا به پایین داریم، همه خطاها در بالا، سپس فقط پاسخ در پایین
اما اکنون آس می آید. اگر فراموش کنیم این یکی را بررسی کنیم چه اتفاقی می افتد:
if (!body.success) {
// handle error (body.error)
return;
}
مسئله این است که ما نمی توانیم. بله، ما باید این بررسی را انجام دهیم. اگر این کار را نکنیم، پس body.data
وجود نخواهد داشت. LSP آن را با پرتاب “داده های “ویژگی” در نوع “ایمن” وجود ندارد به ما یادآوری می کند. و این همه به لطف ساده است Safe
نوع ما ایجاد کردیم و همچنین برای پیام خطا کار می کند. ما دسترسی نداریم body.error
تا زمانی که بررسی کنیم !body.success
.
در اینجا لحظه ای است که ما باید واقعاً از TypeScript و اینکه چگونه جهان جاوا اسکریپت را تغییر داد قدردانی کنیم.
همین امر در مورد:
if (!response.success) {
// handle error (response.error)
return;
}
ما نمی توانیم حذف کنیم !response.success
بررسی کنید زیرا در غیر این صورت response.data
وجود نخواهد داشت.
البته، راه حل ما بدون مشکل نیست، بزرگترین راه حل این است که شما باید به یاد داشته باشید که Promises / عملکردهایی را که می توانند خطاها را با ما ایجاد کنند، بپیچید. safe
لفاف این «ما باید بدانیم» یک محدودیت زبانی است که نمی توانیم بر آن غلبه کنیم.
شاید سخت به نظر برسد، اما اینطور نیست. به زودی متوجه می شوید که تقریباً تمام Promises که در کد خود دارید می توانند خطا ایجاد کنند، و توابع همزمانی که می توانند، شما در مورد آنها می دانید، و تعداد زیادی از آنها وجود ندارد.
با این حال، ممکن است بپرسید آیا ارزشش را دارد؟ ما فکر می کنیم اینطور است و در تیم ما کاملاً کار می کند :). وقتی به یک فایل سرویس بزرگتر نگاه میکنید، بدون هیچکجا امتحان/گرفتن، با هر خطایی که در جایی که ظاهر میشود مدیریت میشود، با یک جریان منطقی خوب… فقط خوب به نظر میرسد.
در اینجا یک کاربرد واقعی (SvelteKit FormAction) آورده شده است:
export const actions = {
createEmail: async ({ locals, request }) => {
const end = perf("CreateEmail");
const form = await safe(request.formData());
if (!form.success) {
return fail(400, { error: form.error });
}
const schema = z
.object({
emailTo: z.string().email(),
emailName: z.string().min(1),
emailSubject: z.string().min(1),
emailHtml: z.string().min(1),
})
.safeParse({
emailTo: form.data.get("emailTo"),
emailName: form.data.get("emailName"),
emailSubject: form.data.get("emailSubject"),
emailHtml: form.data.get("emailHtml"),
});
if (!schema.success) {
console.error(schema.error.flatten());
return fail(400, { form: schema.error.flatten().fieldErrors });
}
const metadata = createMetadata(URI_GRPC, locals.user.key)
if (!metadata.success) {
return fail(400, { error: metadata.error });
}
const response = await new Promise<Safe<Email__Output>>((res) => {
usersClient.createEmail(schema.data, metadata.data, grpcSafe(res));
});
if (!response.success) {
return fail(400, { error: response.error });
}
end();
return {
email: response.data,
};
},
} satisfies Actions;
چند نکته قابل ذکر است:
- عملکرد سفارشی ما
grpcSafe
برای کمک به ما در پاسخ به تماس grpc - CreativeMetadata بازگشت
Safe
در داخل، بنابراین نیازی به پیچیدن آن نیست. -
zod
کتابخانه از همان الگو استفاده می کند 🙂 اگر این کار را نکنیمschema.success
بررسی کنید، ما دسترسی نداریمschema.data
.
به نظر تمیز نمیاد؟ بنابراین آن را امتحان کنید! شاید برای شما هم مناسب باشد 🙂
همچنین امیدوارم این مقاله برای شما جالب بوده باشد. امیدوارم بتوانم تعداد بیشتری از آنها را برای به اشتراک گذاشتن افکار و ایده های خود ایجاد کنم.
PS شبیه به نظر می رسد؟
f, err := os.Open("filename.ext")
if err != nil {
log.Fatal(err)
}
// do something with the open *File f
const response = await safe(fetch("https://example.com"));
if (!response.success) {
console.error(response.error);
return;
}
// do something with the response.data