برنامه نویسی

به روزرسانی به 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();
حالت تمام صفحه را وارد کنید

از حالت تمام صفحه خارج شوید

نوشته های مشابه

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *

دکمه بازگشت به بالا