نموذجي: سلوك غير سليم أو تساهل في الموثوقية

الهدف هو إظهار أين يعطي TS وهم الأمن ، مما يسمح لك بالحصول على أخطاء أثناء تشغيل البرنامج.

لن نتحدث عن الخلل ، في TS يوجد ما يكفي من
1500 بق مفتوح و 6000 مغلق ('هو: المشكلة هي: تسمية مفتوحة: خطأ').

سيتم النظر في جميع الأمثلة مع:

  • وضع TS الصارم قيد التشغيل (كتب مقالًا أثناء الفهم)
  • بدون صريح "any": "as any"، "Objects"، "Function"، {[key: string]: unknown}
  • بدون "أي" ضمنيًا: (noImplicitAny): عمليات الاستيراد غير المنسوخة (ملفات JS الخالصة) ، استدلال النوع غير صحيح
  • بدون تخمينات خاطئة حول الأنواع: استجابة من الخادم ، طباعة مكتبات جهات خارجية

المحتوى:

  • المقدمة
  • الأنواع الاسمية ، الأنواع المخصصة - عندما تبدو الأشياء كما هي ، ولكنها مختلفة جدًا
  • تباين النوع ، الأنواع الدقيقة - حول العلاقة بين الأنواع
  • إبطال التحسين - الحديث عن الثقة
  • الاستثناءات - هل يستحق الاعتراف عند العبث؟
  • العمليات غير الآمنة - الثقة ليست دائمًا جيدة
  • حالات المكافأة - التحقق من النوع في مرحلة مراجعة العلاقات العامة
  • استنتاج

المقدمة


هل من الصعب كتابة دالة لإضافة رقمين في شبيبة؟ اتخاذ تنفيذ ساذج

function sum(a, b) {
	return a + b;
}

دعونا نتحقق من تطبيقنا لـ `sum (2، 2) === 4` ، هل يبدو أن كل شيء يعمل؟ ليس في الحقيقة ، عندما نصف وظيفة ، يجب أن نفكر في جميع أنواع قيم المدخلات ، وكذلك ما يمكن أن تعيده الدالة

1.1 + 2.7   // 3.8000000000000003
NaN + 2     // NaN
99999999999999992 + 99999999999999992 // 200000000000000000
2n + 2      // Uncaught TypeError: 
            // Cannot mix BigInt and other types, use explicit conversions.
{} + true   // 1
2 + '2'     // '22'

Soundness هي قدرة المحلل على إثبات عدم وجود أخطاء أثناء تشغيل البرنامج. إذا تم قبول البرنامج من قبل المحلل ، فمن المؤكد أنه آمن.

البرنامج الآمن هو برنامج يمكنه العمل إلى الأبد دون أخطاء. أولئك. البرنامج لن يتلف أو يرمي الأخطاء.

برنامج صحيح - برنامج يقوم بما ينبغي ولا يفعل ما لا ينبغي. يعتمد التصحيح على تنفيذ منطق الأعمال.

يمكن للأنواع أن تثبت أن البرنامج ككل آمن ، ويختبر أن البرنامج آمن وصحيح فقط ضمن بيانات الاختبار (تغطية 100٪ ، وغياب "المسوخ" من stryker ، واجتياز اختبار قائم على الممتلكات وما إلى ذلك لا يمكن إثبات أي شيء ، والتراخيص يقلل من المخاطر). هناك أساطير تثبت النظرية أنها يمكن أن تثبت صحة البرنامج.

من المهم فهم فلسفة TS ، لفهم ما تحاول الأداة حله ، وما هو مهم ، وما لا تحاول حله. تتخطى

ملاحظة حول Soundness
TS بعض العمليات غير المؤكدة في مرحلة التجميع. تم التفكير بعناية في الأماكن ذات السلوك غير السليم.

أهداف التصميم
ليست الهدف من TS - لإنشاء نظام نوع مع ضمان للأمان ، وبدلاً من ذلك التركيز على التوازن بين السلامة والإنتاجية

مثال الهيكل:
المشكلة ليست السلوك الآمن ، القائمة قد لا تكون كاملة ، هذا ما وجدته في المقالات والتقارير ، قضايا بوابة TS.
الاقتراح هو قضية TS مفتوحة قبل 3-4 سنوات ، مع مجموعة من التعليقات والتفسيرات المثيرة للاهتمام من قبل المؤلفين
نصيحة - IMHO للمؤلف ، ما يعتبره المؤلف الممارسات الجيدة

الكتابة الهيكلية مقابل الكتابة الاسمية


الكتابة الهيكلية مقابل الكتابة الاسمية 1. المشكلة


الكتابة الهيكلية - عندما لا تراعي مقارنة الأنواع أسمائها أو مكان الإعلان عنها ، ويتم مقارنة الأنواع وفقًا لـ "البنية".

نريد إرسال الحرف `sendEmail` إلى العنوان الصحيح` ValidatedEmail` ، وهناك وظيفة للتحقق من العنوان` validateEmail` الذي يُرجع العنوان الصحيح` ValidatedEmail`. للأسف TS يسمح لك بإرسال أي سلسلة إلى `sendEmail` ، لأن `ValidatedEmail` لـ TS لا يختلف عن` string`

type ValidatedEmail = string;
declare function validateEmail(email: string): ValidatedEmail;

declare function sendEmail(mail: ValidatedEmail): void;
sendEmail(validateEmail("asdf@gmail.com"));

// Should be error!
sendEmail("asdf@gmail.com");

الكتابة الهيكلية مقابل الكتابة الاسمية 1. العرض


github.com/microsoft/TypeScript/issues/202
أدخل الكلمة الأساسية "الاسمية" حتى يتم تحديد الأنواع بشكل رمزي. الآن يمكننا منع تمرير `سلسلة` فقط حيث يتوقع` ValidatedEmail`

nominal type ValidatedEmail = string;
declare function validateEmail(email: string): ValidatedEmail;

declare function sendEmail(mail: ValidatedEmail): void;
sendEmail(validateEmail('asdf@gmail.com'));

// Error!
sendEmail('asdf@gmail.com');

الكتابة الهيكلية مقابل الاسمية 1. تلميح


يمكننا إنشاء نوع `Opaque` ، والذي سيأخذ بعضًا من` T` وإعطائه تفردًا من خلال دمجه مع نوع تم إنشاؤه من `K` الذي تم تمريره. يمكن أن يكون `K` إما رمزًا فريدًا (` رمزًا فريدًا`) أو سلسلة (عندئذٍ سيكون من الضروري التأكد من أن هذه السلاسل فريدة).

type Opaque<K extends symbol | string, T> 
	= T & { [X in K]: never };

declare const validatedEmailK: unique symbol;
type ValidatedEmail = Opaque<typeof validatedEmailK, string>;
// type ValidatedEmail = Opaque<'ValidatedEmail', string>;

declare function validateEmail(email: string): ValidatedEmail;

declare function sendEmail(mail: ValidatedEmail): void;
sendEmail(validateEmail('asdf@gmail.com'));

// Argument of type '"asdf@gmail.com"' is not assignable
//  to parameter of type 'Opaque<unique symbol, string>'.
sendEmail('asdf@gmail.com');

الكتابة الهيكلية مقابل الكتابة الاسمية 2. المشكلة


لدينا فئة الدولار واليورو ، كل فئة لديها طريقة إضافة لإضافة الدولار إلى الدولار واليورو إلى اليورو. بالنسبة لـ TS ، هذه الفئات متساوية هيكليًا ويمكننا إضافة الدولار إلى اليورو.

export class Dollar {
  value: number;

  constructor(value: number) {
    this.value = value;
  }

  add(dollar: Dollar): Dollar {
    return new Dollar(dollar.value + this.value);
  }
}

class Euro {
  value: number;

  constructor(value: number) {
    this.value = value;
  }

  add(euro: Euro): Euro {
    return new Euro(euro.value + this.value);
  }
}

const dollars100 = new Dollar(100);
const euro100 = new Euro(100);

// Correct
dollars100.add(dollars100);
euro100.add(euro100);

// Should be error!
dollars100.add(euro100);

الكتابة الهيكلية مقابل الكتابة الاسمية 2. العرض


github.com/microsoft/TypeScript/issues/202
الجملة هي نفسها ، مع "الاسمي" ، ولكن منذ نظرًا لأن الفصول يمكن أن تصبح اسمًا سحريًا (المزيد عن ذلك لاحقًا) ، يتم النظر في إمكانيات إجراء مثل هذا التحول بطريقة أكثر وضوحًا.

الكتابة الهيكلية مقابل الاسمية 1. تلميح


إذا كان للفصل حقل خاص (أصلي بـ `#` أو من TS c `خاص`) ، فعندئذ يصبح الفصل بطريقة سحرية ، يمكن أن يكون الاسم والقيمة أي شيء. يتم استخدام `!` (تأكيد التعيين المحدد) لمنع TS من الشتائم في حقل غير مهيأ (يتم تمكين العلامات الصارمة ، علامات صارمة على خصائص التعريف).

class Dollar {
  // #desc!: never;
  private desc!: never;
  value: number;

  constructor(value: number) {
    this.value = value;
  }

  add(dollar: Dollar) {
    return new Dollar(dollar.value + this.value);
  }
}

class Euro {
  // #desc!: never;
  private desc!: never;
  value: number;

  constructor(value: number) {
    this.value = value;
  }

  add(euro: Euro) {
    return new Euro(euro.value + this.value);
  }
}

const dollars100 = new Dollar(100);
const euro100 = new Euro(100);

// Correct
dollars100.add(dollars100);
euro100.add(euro100);

// Error: Argument of type 'Euro' is not assignable to parameter of type 'Dollar
dollars100.add(euro100);

تباين النوع 1. المشكلة


خيار البرمجة ، باختصار ، هو القدرة على تمرير Supertype / Subtype هناك ، حيث يتوقع النوع. على سبيل المثال ، هناك شكل هرمي -> دائرة -> مستطيل ، هل من الممكن نقل أو إرجاع شكل / مستطيل إذا كان من المتوقع وجود دائرة؟

البديل في برمجة الهابر ، SO .

يمكننا تمرير النوع مع الحقل الذي يقع فيه الرقم إلى دالة تتوقع الحقل كسلسلة أو رقم ، وتحوير الكائن المرسل في الجسم ، وتغيير الحقل إلى سلسلة. أولئك. `{status: number} مثل {status: number | string} مثل {status: string} `هنا خدعة مثل تحويل رقم إلى سلسلة ، مما يتسبب في حدوث خطأ مفاجئ .

function changeStatus(arg: { status: number | string }) {
  arg.status = "NotFound";
}

const error: { status: number } = { status: 404 };
changeStatus(error);

// Error: toFixed is not a function
console.log(error.status.toFixed());

نوع التباين 1. عرض


github.com/Microsoft/TypeScript/issues/10717
يُقترح إدخال "داخل / خارج" للحد صراحةً من التغاير / التعارض في الأدوية الجنسية.

function changeStatus<
  out T extends {
    status: number | string;
  }
>(arg: T) {
  arg.status = "NotFound";
}

const error: { status: number } = { status: 404 };
// Error!
changeStatus(error);

console.log(error.status.toFixed());

نوع التباين 1. نصيحة


إذا عملنا بهياكل غير قابلة للتغيير ، فلن يكون هناك مثل هذا الخطأ (لقد قمنا بالفعل بتمكين إشارة نوع الوظيفة الصارمة).

function changeStatus(arg: Readonly<{ status: number | string }>) {
  // Error: Cannot assign, status is not writable
  arg.status = "NotFound";
}

const error: Readonly<{ status: number }> = { status: 404 };
changeStatus(error);

console.log(error.status.toFixed());

نوع التباين 1. مكافأة


Readonly قابل للتخصيص على
github.com/Microsoft/TypeScript/issues/13347
github.com/microsoft/TypeScript/pull/6532#issuecomment-174356151

ولكن حتى إذا أنشأنا نوع Readonly ، فلن يمنع TS من الانتقال إلى الوظيفة حيث غير متوقع للقراءة فقط `Readonly <{readonly status: number}> مثل {status: number | string} مثل {status: string} `

function changeStatus(arg: { status: number | string }) {
  arg.status = "NotFound";
}

const error: Readonly<{ readonly status: number }> 
  = { status: 404 };
changeStatus(error);

// Error: toFixed is not a function
console.log(error.status.toFixed());

نوع التباين 2. المشكلة


قد تحتوي الكائنات على حقول إضافية لا تحتوي عليها الأنواع المقابلة لها: `{message: string؛ status: string} مثل {message: string} `. بسبب بعض العمليات قد لا تكون آمنة

const error: { message: string; status: string } = {
  message: "No data",
  status: "NotFound"
};

function updateError(arg: { message: string }) {
  const defaultError = { message: "Not found", status: 404 };
  const newError: { message: string; status: number }
    = { ...defaultError, ...arg };
  
  // Error: toFixed is not a function
  console.log(newError.status.toFixed());
}

updateError(error);

يعتقد TS أنه نتيجة لعملية الدمج ، ستكون حالة {... {message: string، status: number}، ... {message: string}} `رقمًا.

في الواقع ، `{... {message:" Not found "، status: 404}، ... {message:" No data "، status:" NotFound "}،}` status - string.

نوع التباين 2. العرض


github.com/microsoft/TypeScript/issues/12936
تقديمه ل`نوع Exact` أو بناء جملة مماثل القول بأن نوع لا يمكن أن تحتوي حقول إضافية.

const error: Exact<{ message: string; }> = {
  message: "No data",
};

function updateError(arg: Exact<{ message: string }>) {
  const defaultError = {  message: "Not found", status: 404, };
  // Can spread only Exact type!
  const newError = { ...defaultError, ...arg };
  console.log(newError.status.toFixed());
}

updateError(error);

نوع التباين 2. تلميح


دمج الكائنات من خلال سرد الحقول بشكل صريح أو تصفية الحقول غير المعروفة.

const error: { message: string; status: string } = {
  message: "No data",
  status: "NotFound"
};

function updateError(arg: { message: string }) {
  const defaultError = { message: "Not found", status: 404 };
  // Merge explicitly or filter unknown fields
  const newError = { ...defaultError, message: arg.message };
  console.log(newError.status.toFixed());
}

updateError(error);

إبطال التنقية. مشكلة


بعد أن أثبتنا شيئًا بشأن الحالة الخارجية ، فإن وظائف الاتصال ليست آمنة ، لأن لا توجد ضمانات بأن الوظائف لا تغير هذه الحالة الخارجية:

export function logAge(name: string, age: number) {
  // 2nd call -  Error: toFixed is not a function
  console.log(`${name} will lose ${age.toFixed()}`);
  person.age = "PLACEHOLDER";
}

const person: { name: string; age: number | string } = {
  name: "Person",
  age: 42
};

if (typeof person.age === "number") {
  logAge(person.name, person.age);
  // refinement should be invalidated
  logAge(person.name, person.age);
}

إبطال التنقية. جملة او حكم على


github.com/microsoft/TypeScript/issues/7770#issuecomment-334919251
إضافة `pure` معدل للوظائف، هذه الإرادة على الأقل تسمح لك أن تثق هذه الوظائف

إبطال التنقية. تلميح


استخدم بنيات البيانات غير القابلة للتغيير ، عندها ستكون استدعاء الوظيفة آمنة مسبقًا لعمليات الفحص السابقة.

علاوة


نوع التدفق قوي لدرجة أنه لا يحتوي على جميع المشاكل المذكورة أعلاه ، ولكنه يعمل بحيث لا أوصي باستخدامه.

الاستثناءات. مشكلة


لا يساعد TS في العمل مع الاستثناءات بأي شكل من الأشكال ؛ لا يوجد شيء واضح على توقيع الوظيفة.

import { JokeError } from "../helpers";

function getJoke(isFunny: boolean): string {
  if (isFunny) {
    throw new JokeError("No funny joke");
  }
  return "Duh";
}

const joke: string = getJoke(true);
console.log(joke);

الاستثناءات. جملة او حكم على


github.com/microsoft/TypeScript/issues/13219
يقترح إدخال بناء جملة يسمح بوصف الاستثناءات بوضوح في توقيع الوظيفة

import { JokeError } from '../helpers';

function getJoke(isFunny: boolean): string | throws JokeError {
  /*...*/}

function getJokeSafe(isFunny: boolean): string {
  try {
    return getJoke(isFunny);
  } catch (error) {
    if (error instanceof JokeError) {
      return "";
    } else {
      // Should infer error correctly, should cast to never
      return error as never;
    }
  }
}

console.log(getJokeSafe(true));

الاستثناءات. علاوة


github.com/microsoft/TypeScript/issues/6283
لسبب ما ، في TS ، يتجاهل تعريف نوع Promise نوع الخطأ

const promise1: Promise<number> = Promise.resolve(42);

const promise: Promise<never> = Promise.reject(new TypeError());

// typescript/lib
interface PromiseConstructor {
  new <T>(
    executor: (
      resolve: (value?: T | PromiseLike<T>) => void,
      reject: (reason?: any) => void
    ) => void
  ): Promise<T>;
}

الاستثناءات. تلميح


خذ أيًا من الحاوية مثل Promise ، مع أفضل كتابة فقط. ( إما مثال على التنفيذ )

import { Either, exhaustiveCheck, JokeError } from "../helpers";

function getJoke(isFunny: boolean): Either<JokeError, string> {
  if (isFunny) {
    return Either.left(new JokeError("No funny joke"));
  }
  return Either.right("Duh");
}

getJoke(true)
  // (parameter) error: JokeError
  .mapLeft(error => {
    if (error instanceof JokeError) {
      console.log("JokeError");
    } else {
      exhaustiveCheck(error);
    }
  })
  // (parameter) joke: string
  .mapRight(joke => console.log(joke));

عمليات غير آمنة. مشكلة


إذا كان لدينا مجموعة ذات حجم ثابت ، فيمكن أن تضمن TS وجود شيء في الفهرس المطلوب. لن يعمل هذا مع المصفوفة وسوف يثق بنا TS

// ReadOnly fixed size tuple
export const constNumbers: readonly [1, 2, 3] 
  = [1, 2, 3] as const;

// Error: Object is possibly 'undefined'.
console.log(constNumbers[100].toFixed());

const dynamicNumbers: number[] = [1, 2, 3];
console.log(dynamicNumbers[100].toFixed());

عمليات غير آمنة. جملة او حكم على


github.com/microsoft/TypeScript/issues/13778
يُقترح إضافة `undefined` إلى نوع الإرجاع` T` للوصول إلى الفهرس إلى الصفيف. ولكن في هذه الحالة ، عند الوصول إلى أي فهرس ، سيكون عليك استخدام "؟" أو إجراء فحوص صريحة.

// interface Array<T> {
//   [n: number]: T | undefined;
// }

const dynamicNumbers: number[] = [1, 2, 3];
// Error: Object is possibly 'undefined'.
console.log(dynamicNumbers[100].toFixed());

// Optional chaining `?`
console.log(dynamicNumbers[100]?.toFixed());

// type refinement
if (typeof dynamicNumbers[100] === 'number') {
  console.log(dynamicNumbers[100].toFixed());
}

عمليات غير آمنة. تلميح


من أجل عدم إنتاج كيانات تتجاوز الحاجة ، نأخذ الحاوية المعروفة سابقًا `إما 'ونكتب وظيفة آمنة للعمل مع الفهرس ، والتي ستعود` إما <فارغة ، T> `.

import { Either } from "../helpers";

function safeIndex<T>(
  array: T[],
  index: number,
): Either<null, T> {
  if (index in array) {
    return Either.right(array[index]);
  }
  return Either.left(null);
}

const dynamicNumbers: number[] = [1, 2, 3];

safeIndex(dynamicNumbers, 100)
  .mapLeft(() => console.log("Nothing"))
  .mapRight(el => el + 2)
  .mapRight(el => console.log(el.toFixed()));

علاوة وظائف إعادة التحميل


إذا أردنا أن نقول أن الدالة تأخذ بضعة أسطر وتعيد سلسلة أو تأخذ عددًا من الأرقام وترجع رقمًا ، فعند التنفيذ ستكون هذه التوقيعات متجاورة ، ويجب على المبرمج ضمان صحتها ، ولكن على TS.

ملاحظة نظرة على البديل من خلال الأنواع العامة والشرطية:

function add(a: string, b: string): string;
function add(a: number, b: number): number;
function add(a: string | number,
             b: string | number,
): string | number {
  return `${a} + ${b}`;
}

const sum: number = add(2, 2);
// Error: toFixed is not a function
sum.toFixed();


علاوة اكتب حارس


تثق TS في المبرمج بأن `isSuperUser` يحدد بشكل صحيح من هو" SuperUser "وإذا تمت إضافة" Vasya "، فلن تكون هناك مطالبات.

PS يستحق التفكير في كيفية تمييز الأنواع الموجودة بالفعل في مرحلة اتحادهم الموسوم

type SimpleUser = { name: string };
type SuperUser = { 
  name: string; 
  isAdmin: true; 
  permissions: string[] 
};
type Vasya = { name: string; isAdmin: true; isGod: true };
type User = SimpleUser | SuperUser | Vasya;

function isSuperUser(user: User): user is SuperUser {
  return "isAdmin" in user && user.isAdmin;
}

function doSomethings(user: User) {
  // Error: Cannot read property 'join' of undefined
  if (isSuperUser(user)) {
    console.log(user.permissions.join(","));
  }
}

استنتاجات على النصائح


- أنواع الاسمية : نوع معتم، وحقول خاصة
- نوع التباين : أنواع المطابقة، نوع DeepReadonly
- الاستثناءات : إما الكائن الدقيق الاحادي الخلية
- صقل إبطال : وظائف الصرفة
- العمليات غير الآمنة (الوصول إلى رقم قياسي) : إما / ربما الكائنات الدقيقة الاحاديه الخلية

غير قابل للتغيير البيانات، وظائف نقية، الكائنات الدقيقة الاحاديه الخلية ... مبروك ، أثبتنا أن FP رائع!

الموجودات


  • تريد TS تحقيق التوازن بين الصحة والإنتاجية
  • يمكن أن تثبت الاختبارات الأمان والصحة لبيانات الاختبار فقط.
  • يمكن أن تثبت الأنواع الأمان العام للبرنامج.
  • طفرة - سيئة ، حسنا؟

كما توصي DZ باللعب مع Flow بعد تصحيح خطأ بسيط:

// https://flow.org/try/
declare function log(arg: { name: string, surname?: string }): void;
const person: { name: string } =  { name: 'Negasi' };
// Error
log(person);

رمز المثال ، حل المشكلة وتحليلها ، روابط مفيدة في المستودع .

All Articles