به روزرسانی به Ember 6.1: Linting با نوع اطلاعات

من اخیراً تمام بسته های موجود در Monorepo بسیار بزرگ خود را به برنامه برنامه EMBER 6.1 ارتقاء داده ام. File File By File بسیار متوسط که به نظر می رسد فقط برخی از وابستگی ها را ارتقا می بخشد و به جدید تغییر می دهد eslint
فرمت فایل پیکربندی مسطح برخی از تغییرات بسیار هیجان انگیز و تأثیرگذار را در اعتبار سنجی کد که eslint
اجرا می کند
درخواست کشش من که به Ember 6.1 ارتقا می یابد و تمام کد ما را به روز می کند تا مطابق با پیکربندی پیش فرض جدید Linting شامل 13 تعهد حاوی تغییر در 12100 خط کد در 1155 پرونده باشد! این تقریباً همه با تغییر از آن هدایت می شد @typescript-eslint
پیکربندی توصیه شده به پیکربندی توصیه شده TYPECHECKED.
من در فرآیند به روزرسانی تمام کد های خود برای رعایت پیکربندی جدید Linting ، چیزهای زیادی آموختم ، و توضیحات PR نسبتاً طولانی و مفصلی را نوشتم تا آنچه را که با تیمم آموخته ام به اشتراک بگذارم ، بنابراین فهمیدم که می خواهم آن را با آن به اشتراک بگذارم جامعه گسترده تر در صورتی که برای دیگران مفید باشد. و اینجاست!
EMBER 6.1 شامل برخی از به روزرسانی های بزرگ لینتینگ است که در زیر شرح داده شده است. بنابراین این روابط عمومی ما را به Ember 6.1 ، از جمله برخی از به روزرسانی های متفرقه/جزئی از طرح های جزئی ، به روز می کند ، اما بخش عمده آن باعث می شود که پایگاه کد ما مطابق با قوانین جدید باشد.
راه اندازی جدید لینت
یک تغییر بسیار قابل توجه در تنظیم لینتینگ این است که ts
وت gts
پرونده ها ، ما شروع به استفاده از Linting با Type Information کرده ایم ، که به ما امکان می دهد از قوانین Linting استفاده کنیم که بسیار قدرتمندتر و مفیدتر هستند. پیامدهای اصلی این در زیر شرح داده شده است.
این قانون به روز شد تا متغیرهای بلااستفاده در آن نیز پرچم گذاری شود catch
اظهارات ، بنابراین جایی که قبلاً انجام می دادیم
try {
doSomething();
} catch (e) {
// ignore
}
اکنون می توانیم اعلامیه متغیر را به طور کامل حذف کنیم و از نحوی که حتی نمی فهمیدم مجاز است استفاده کنیم:
try {
doSomething();
} catch {
// ignore
}
این قانون به ما کمک می کند تا اطمینان حاصل کنیم که با روش ها تماس نمی گیریم this
به اشتباه محدود ، به عنوان مثال
class Thingy {
private readonly obj = { key: 'value' };
setObjValue(val: string) {
this.obj.key = val;
}
}
let thingy = new Thingy();
let setObjValue = thingy.setObjValue;
// whoops, `this` will be `null`, so `this.obj.key`
// with throw an error
setObjValue('hello');
هنگامی که من کد خود را برای رعایت این قانون برطرف کردم ، با برخی از “مثبت های کاذب” روبرو شدم که به چند روش مختلف خطاب کردم و برای نوشتن کد در آینده برای رعایت این قانون آموزنده هستم.
مثبت های کاذب عمدتاً مربوط به تمایز فازی بین یک روش کلاس و خاصیتی است که به نظر می رسد یک تابع است. در نظر بگیرید:
class Thingy {
classMethod() {}
readonly propertyThatIsAFunction = () => {};
}
const thingy = new Thingy();
// unsafe because if `classMethod` references `this`, calling
// `classMethod()` won't have `this` bound correctly
const classMethod = thingy.classMethod;
// safe because arrow functions have `this` "pre-bound"
const propertyThatIsAFunction = thingy.propertyThatIsAFunction;
این فقط برای کلاس ها صدق نمی کند ، بلکه در مورد اشیاء نیز صدق می کند ، یعنی موارد فوق دقیقاً یکسان با یک شیء به جای کلاس کلاس و کلاس است:
const thingy = {
classMethod() {},
propertyThatIsAFunction: someFunction,
};
بنابراین ، هنگامی که این قانون کاری را که شما به عنوان یک خطا انجام می دهید پرچم گذاری می کند ، می توانید یک عملکرد فلش ایجاد کنید:
const classMethod = () => thingy.classMethod();
یا هنگام صرفه جویی در آن ، آن را وصل کنید:
const classMethod = thingy.classMethod.bind(thingy);
یا اگر به کلاس اصلی دسترسی دارید و به نظر می رسد معقول است ، خود را در تعریف کلاس به یک عملکرد پیکان درون خطی تبدیل کنید:
class Thingy {
readonly classMethod = () => {};
}
// or
const thingy = {
classMethod: () => {};
}
any
لینت مرتبط با آن
یکی از خطرات استفاده از مقادیر تایپ شده any
این است که آنها می توانند از آنچه که به نظر می رسد ، به یک ردپای کد بسیار بزرگتر نشت کنند. به عنوان مثال ،
// Suppose `create-object` is typed so that `createObject()` returns
// `{ value: string }`
import createObject from 'create-object';
let object = createObject();
// fine
object.value;
// type error
object.subObject.value;
اکنون ، اگر create-object
فاقد اطلاعات نوع ،
// @ts-expect-error no types
// createObject is typed as `any`
import createObject from 'create-object';
// `object` is typed as `any`
let object = createObject();
// fine
object.value;
// also fine at compile-time, but will throw an error at runtime
object.subObject.value;
اصولاً ، به محض اینکه یک مقدار تایپ شده دارید any
، هر مقداری که از آن به دست می آورید (از طریق دسترسی به ویژگی ها ، تماس های عملکردی و غیره) خواهد بود any
و کامپایلر قادر به انجام هر نوع معناداری در مورد آن مقادیر نخواهد بود.
ما داشته ایم @typescript-eslint/no-explicit-any
برای مدتی فعال شده است ، اما این فقط ما را از تایپ صریح یک مقدار به عنوان جلوگیری می کند any
بشر یک دسته از موارد وجود دارد که یک مقدار می تواند باشد ضمنی تایپ شده به عنوان any
، و این قانون به معنای “حاوی خسارت” است.
موارد بسیاری وجود دارد که این قانون خطاها را پرچم گذاری می کند ، و بسیاری از روش های پرداختن به آن خطاها ، اما در اینجا موارد متداول که من به آن رسیدم وجود دارد:
ذاتاً مقادیر بدون نسخه
هنگام تماس JSON.parse()
یا await fetchResponse.json()
یا از این دست ، هیچ نوع اطلاعات استاتیک وجود ندارد – ما ، برنامه نویس ، باید به کامپایلر بگوییم که چه نوع ارزشی را انتظار داریم از Deserializing رشته JSON به یک شی بدست آوریم. بنابراین در اینجا ، ما فقط باید آن را ریخته کنیم ، به عنوان مثال let myResult = JSON.parse(str) as MyResult;
بشر گاهی اوقات نوع به عنوان یک نوع نامگذاری شده تعریف نمی شود و به نظر نمی رسد ایجاد یک نوع نامگذاری شده ارزشمند باشد ، بنابراین می توانید فقط این کار را انجام دهید let { value } = JSON.parse(str) as { value: string };
بشر
گرفتن بلوک
در JavaScript ، شما می توانید هر چیزی را پرتاب کنید ، نه فقط Error
اشیاء throw 'whoops'
یا حتی throw undefined
کاملاً معتبر هستند (گرچه عمل بد است ، و ما یک قانون لینتینگ برای جلوگیری از انجام این کار در کد خود داریم). بنابراین در بلوک های صید ، حتی اگر ما به طور کلی با ارزش پرتاب شده به عنوان یک رفتار می کنیم Error
، ما در واقع نمی دانیم که چیست.
تا همین اواخر در پیکربندی TypeScript ما (جایی که ما از آن استفاده کرده بودیم که از آن استفاده کرده بودیم) ، متغیرهای موجود در بلوک های گرفتن به عنوان تایپ شدند any
(عمدتاً برای سهولت انتقال کد از جاوا اسکریپت غیر تایپ که در آن ما با انواع خود به TypeScript سریع و شل می کنیم). بنابراین این قانون جدید تقریباً هر نوع استفاده (هه!) از متغیر گرفتار را به عنوان یک خطا پرچم می کند و به یک بازیگران صریح نیاز دارد. NB ، پس از انجام این کار ، کار خیلی اضافی برای فعال کردن نبود useUnknownInCatchVariables
، که من انجام دادم ، بنابراین اکنون این متغیرها در catch
بلوک ها به صورت تایپ می شوند unknown
و کامپایلر (بر خلاف آستانه) به ما اجازه نمی دهد بدون ریخته گری/نوع-دستکاری ، با آنها کاری انجام دهیم.
برای اینکه تا حد ممکن ریسک پذیر باشیم ، باید همیشه از آن استفاده کنیم instanceof
بررسی یا نگهبانان نوع یا هر چیز دیگری برای اطمینان از اینکه چیز پرتاب شده از نوعی است که با آنچه انتظار داریم سازگار باشد ، به عنوان مثال
try {
doThing();
} catch (e) {
if (e instanceof Error) {
reportErrorMessage(e.message);
} else if (typeof e === 'string') {
reportErrorMessage(e);
} else {
reportErrorMessage('unknown error');
}
}
اما این یک درد بزرگ است ، و با توجه به اینکه ما در برابر پرتاب اشیاء غیر خطا برای کد خود ، به طور کلی باید یک مورد لبه باشد ، بنابراین در مواردی مانند موارد فوق ، من تقریباً همیشه چنین کاری را انجام می دادم
try {
doThing();
} catch (e) {
reportErrorMessage((e as Error).message);
}
و من فکر می کنم این کار برای ما خوب است که به طور کلی انجام دهیم مگر اینکه در منطقه ای از کد قرار بگیریم که به خصوص نگران آنچه ممکن است پرتاب شود (به عنوان مثال از کد خارجی) یا می خواهیم دفاعی فوق العاده باشیم.
مقادیر خطای دارای نوع و دارای تایپ ضعیف
منظور من از “مقادیر خطای تایپ” است که در آن استفاده می کنیم @ts-expect-error
برای اینکه به کامپایلر بگویید نگران این واقعیت نیست که ما کاری را انجام می دهیم که آن را درک نمی کند ، و سپس ارزش هایی را که از آن بیانیه بیرون می آید تایپ می کند any
بشر منظور من از مقادیر ضعیف است ، منظورم مکانهایی است که ما عمداً از “هر” مانند استفاده می کنیم context
در ModalManager
(به طور معمول به این دلیل که ما هنوز تلاش نکرده ایم تا بفهمیم چگونه می توان آن را به درستی تایپ کرد).
در هر دو مورد ، اگر نمی توانید مشکل اساسی را برطرف کنید ، فقط بازیگران است. به عنوان مثال ،
// @ts-expect-error no types
import createObject from 'create-object';
let obj = createObject() as {
key: string;
stats: { count: number }
};
let count = obj.stats.count;
یا حتی (گرچه اولی احتمالاً در بیشتر موارد ارجح است)
// @ts-expect-error no types
import createObject from 'create-object';
// leave it typed as `any`
let obj = createObject();
// we don't care about `key` here, so don't bother to include it
let count = (obj as { stats: { count: number } }).stats.count;
وعده/لینک مربوط به async
اکنون ما قوانینی را در اختیار داریم که مجموعه ای از موارد مربوط به Async/Atait و وعده ها را تأیید می کند ، و ما را وادار می کند تا در مورد کار با Async/Pression خود ، که کد ایمن تری را تولید می کند ، متفکرتر و عمدی تر باشیم. دو قانون مهم عبارتند از:
@TypeScript-Eslint/نیاز-انتظار
این قانون مستلزم آن است async
توابع یا دارای await
بیانیه در آنها ، یا صریحاً یک وعده را برگردانید. وقتی این قانون خطایی را پرچم گذاری می کند ، احتمالاً می خواهید فقط موارد را حذف کنید async
کلمه کلیدی از عملکرد ، آن را همزمان می کند. با این حال ، در بعضی موارد ممکن است شما عملکرد را به یک API منتقل کنید که به یک عملکرد بازپرداخت قول نیاز دارد ، بنابراین فقط حذف کنید async
کلمه کلیدی یک خطای نوع ایجاد می کند. در این حالت می توانید async
و هر مقادیر بازگشت را در Promise.resolve()
به عنوان مثال return Promise.resolve('result')
یا فقط return Promise.resolve()
اگر عملکرد برگردد void
بشر
@TypeScript-Eslint/بدون شناور
این قانون ما را از “نادیده گرفتن” وعده های خودداری می کند. به عنوان مثال ، اگر با async
عملکرد ، شما باید یکی از موارد زیر را انجام دهید:
// await the returned promise
await asyncThing();
// store the promise in a variable
const promise = asyncThing();
// pass the promise to a function
handlePromise(asyncThing());
// apply a `.catch()` handler to it
asyncThing().catch((e) => /* ... */);
// explicitly ignore it with the `void` operator
void asyncThing()
این به معنای اطمینان از این است که ما به طور تصادفی وعده هایی را که باید از آن استفاده کنیم ، دور نمی کنیم ، هم برای جلوگیری از خطاهای Async/Race/زمان بندی ، و اطمینان حاصل کنیم که اگر این قول رد شود ، این یک رد غیرقانونی نیست (که می تواند باعث می شود که ما خطاها را از دست ندهیم یا در گره ، باعث خروج روند) شود.
این قانون ما را وادار می کند تا با دقت بیشتری در مورد استفاده ما از توابع ناهمزمان فکر کنیم ، و اینکه آیا آنها در سطح API غیر همزمان هستند ، یا اینکه آیا آنها فقط به عنوان یک جزئیات اجرای داخلی رفتار ناهمزمان دارند. در نظر بگیرید:
async function getFromServer(url: string) {
let response = await fetch(url);
return response.json();
}
async function setAndTrack(obj: Thing) {
obj.value = 'hello';
// get some stats from the object (async operation) and send them to a metrics service
let stats = await obj.getStats();
await sendTracksToMetricsService(stats);
}
در مورد اول ، تماس گیرنده await
تماس عملکرد ، و async-ness در واقع بخشی از API عملکرد است-گرفتن از سرور ذاتاً ناهمزمان است و تماس گیرنده باید منتظر نتیجه باشد. در مورد دوم ، تماس گیرنده این کار را نمی کند await
تماس عملکرد به دلیل ردیابی معیارها یک اثر جانبی است و این واقعیت که ناهمزمان است ، جزئیات اجرای آن است. بنابراین API عملکرد دوم همزمان است ، ما فقط عملکرد را می سازیم async
بنابراین ما راحت می گیریم await
نحو ، به جای اینکه مجبور به استفاده از زنجیره ای وعده شود:
function setBroadcast(obj: Thing) {
obj.value = 'hello';
// get some stats from the object (async operation) and send them to a metrics service
obj.getStats().then((stats) =>
sendTracksToMetricsService(stats)
).catch((e) => /* handle error */);
}
بنابراین ، اگر اصل داشتیم async
اجرای setBroadcast
، درست است که بدون آن تماس بگیرید await
آن را به این دلیل که تماس گیرنده نمی خواهد قبل از ادامه کار منتظر ردیابی معیارها باشد. اما این باعث ایجاد خطای لینت می شود. در این حالت ، به ما می گوید که API عملکرد باید واقعاً همزمان باشد ، و ما باید عملکرد را بازنویسی کنیم تا نباشد async
(به جای اینکه همه سایت های تماس اضافه کنید void
یا catch()
).
در این حالت ، ما می توانیم کیک خود را تهیه کنیم و آن را با استفاده از یک Async Iife بخوریم:
function setBroadcast(obj: Thing) {
obj.value = 'hello';
(async () => {
// get some stats from the object (async operation) and send them to a metrics service
let stats = await obj.getStats();
await sendTracksToMetricsService(stats);
})().catch((e) => /* handle error */);
}
در مواردی که ما واقعاً می خواهیم یک عملکرد async را فراخوانی کنیم اما منتظر تکمیل آن نباشیم قبل از ادامه کار ، احتمالاً بهتر است
doAsyncThing().catch(captureException);
یا در موارد بسیار نادری که ما کاملاً مطمئن هستیم که خطایی نخواهد داشت
void doReallySafeAsyncThing();