نوع‌های نال نپذیر در TypeScript
اندازه‌ی قلم متن
تخمین مدت زمان مطالعه‌ی مطلب: نه دقیقه

تا پیش از ارائه‌ی کامپایلر TypeScript 2.0، مقادیر null و undefined، به هر نوعی قابل انتساب بودند و امکان تفکیک آن‌ها وجود نداشت که این مورد می‌تواند منشاء بروز بسیاری از خطاهای در زمان اجرا شود.
let name: string;
name = "Vahid"; // OK
name = null; // OK
name = undefined;  // OK
let age: number;
age = 24; // OK
age = null; // OK
age = undefined;  // OK
برای نمونه در اینجا یک متغیر رشته‌ای و همچنین عددی تعریف شده‌اند که انتساب null و یا undefined نیز به آن‌ها مجاز است. این مورد جهت نوع‌های ورودی و خروجی متدها، اشیاء و آرایه‌ها نیز میسر است.


نوع null در TypeScript

همانند JavaScript، نوع null تنها یک مقدار معتبر نال را می‌تواند داشته باشد و نمی‌توان برای مثال یک رشته را به آن انتساب داد. اما انتساب این مقدار به هر نوع متغیر دیگری، سبب پاک شدن مقدار آن خواهد شد. با فعالسازی strictNullChecks، این نوع را تنها به نوع‌های نال‌پذیر می‌توان انتساب داد.


نوع undefined در TypeScript

هر متغیری که مقداری به آن انتساب داده نشده باشد، با undefined مقدار دهی می‌شود. این مورد حتی جهت خروجی متدها نیز صادق است و اگر return ایی در آن‌ها فراموش شود، این خروجی نیز به undefined تفسیر می‌شود.
در اینجا نیز اگر نوع متغیری به undefined تنظیم شد، این متغیر تنها مقدار undefined را می‌تواند بپذیرد. تنها با خاموش کردن پرچم strictNullChecks می‌توان آن‌را به اعداد، رشته‌ها و غیره نیز انتساب داد.


فعالسازی نوع‌های نال نپذیر در TypeScript

برای فعالسازی این قابلیت، نیاز است پرچم strictNullChecks را در فایل تنظیمات کامپایلر به true تنظیم کرد:
{
    "compilerOptions": {
        "strictNullChecks": true
    }
}
از این پس دیگر نمی‌توان null و undefined را به هر نوعی انتساب داد و این‌ها تنها به خودشان و یا نوع any، قابل انتساب هستند. برای مثال اکنون نوع number فقط یک عدد است و دیگر قابلیت پذیرش null و یا undefined را ندارد. البته در اینجا یک استثناء هم وجود دارد: undefined را می‌توان به نوع void نیز انتساب داد.
برای مثال اگر متدی، رشته‌ای را به عنوان پارامتر قبول کند، تا پیش از TypeScript 2.0 و فعالسازی strictNullChecks آن، مشخص نبود که رشته‌ی دریافتی از آن واقعا یک رشته‌است و یا شاید null. اما اکنون یک رشته، فقط یک رشته‌است و دیگر نال پذیر نیست.
 let foo: string = null; // Error! Type 'null' is not assignable to type 'string'.
به این ترتیب دیگر به خطاهای زمان اجرایی مانند خطاهای ذیل نخواهیم رسید:
Uncaught ReferenceError: foo is not defined
Uncaught TypeError: window.foo is not a function

این مورد برای آرایه‌ها نیز صادق است:
// With strictNullChecks set to false
let d: Array<number> = [null, undefined, 10, 15]; //OK
let e: Array<string> = ["pie", null, ""];  //OK
 
 
// With strictNullChecks set to true
let d: Array<number> = [null, undefined, 10, 15]; // Error
let e: Array<string> = ["pie", null, ""]; // Error
اگر strictNullChecks فعال شود، دیگر نمی‌توان به اعضای یک آرایه مقادیر null و یا undefined را نسبت داد.


ساده سازی تعریف بررسی‌های با پرچم strict، در TypeScript 2.3

تعداد گزینه‌های قابل تنظیم در فایل tsconfig روز به روز بیشتر می‌شوند. به همین جهت برای ساده سازی فعالسازی آن‌ها، از TypeScript 2.3 به بعد، پرچم strict نیز به این تنظیمات اضافه شده‌است. کار آن فعالسازی یکجای تمام بررسی‌های strict است؛ مانند noImplicitAny، strictNullChecks و غیره.
{ 
    "compilerOptions": { 
        "strict": true  /* Enable all strict type-checking options. */ 
    } 
}
در این حالت اگر نیاز به لغو یکی از گزینه‌ها بود، می‌توان به صورت ذیل عمل کرد:
{ 
    "compilerOptions": { 
        "strict": true, 
        "noImplicitThis": false 
    } 
}
گزینه‌ی strict تمام بررسی‌های متداول را فعال می‌کند؛ اما ذکر و تنظیم صریح noImplicitThis به false، تنها این یک مورد را لغو خواهد کرد.

یک نکته: اجرای دستور tsc --init ، سبب تولید یک فایل tsconfig.json از پیش تنظیم شده، بر اساس آخرین قابلیت‌های کامپایلر TypeScript می‌شود.


اما ... اکنون چگونه یک نوع را نال‌پذیر کنیم؟

TypeScript به همراه دو نوع ویژه‌ی null و undefined نیز شده‌است که تنها دارای مقادیر null و undefined می‌توانند باشند. به این معنا که در حین تعریف نوع یک متغیر، می‌توان این دو را نیز ذکر کرد و دیگر تنها به عنوان دو مقدار مطرح نیستند. به این ترتیب می‌توان از آن‌ها یک union type را ایجاد کرد:
 let foo: string | null = null; // Okay!
اکنون تنها در این حالت است که متغیر foo می‌تواند یک رشته و یا یک null را دریافت کند و یا اگر مثال ابتدای بحث را بخواهیم اصلاح کنیم، به نمونه‌ی ذیل خواهیم رسید:
let name: string | null;
name = "Vahid"; // OK
name = null; // OK
name = undefined;  // Error
یکی دیگر از مزایای این روش، وضوح بیشتر تعریف نوع متغیرها و به نوعی «خود مستند سازی» بهتر آن‌ها است. در این حالت یا به صورت صریح مشخص می‌کنیم که متدی فقط یک رشته را می‌پذیرد و یا با ذکر string | null، به استفاده کننده اعلام می‌کنیم که ارسال null نیز به آن پیش بینی شده‌است و به نتیجه‌ی نامشخصی منتهی نخواهد شد.

یک نکته:
تا پیش از این اگر متغیری را به این صورت تعریف می‌کردیم:
let z = null;
نوع آن any درنظر گرفته می‌شد. اما اکنون، نوع آن تنها null است و تنها مقداری را هم که می‌تواند بپذیرد نال خواهد بود.


بررسی انتساب، پیش از استفاده

با فعالسازی strictNullChecks، اکنون کامپایلر برای تمام نوع‌هایی که undefined نیستند، یک مقدار اولیه را پیش از استفاده‌ی از آن‌ها درخواست می‌کند:
testAssignedBeforeUseChecking() {
    let x: number;
    console.log(x);
}
در اینجا چون x از نوع عددی است، به علت عدم مقدار دهی اولیه، قابلیت استفاده‌ی از آن وجود ندارد و کامپایلر خطای ذیل را اعلام می‌کند:
 [ts] Variable 'x' is used before being assigned.

اما در حالت ذیل، عدد z می‌تواند عدد و یا undefined باشد؛ به همین جهت کامپایلر با استفاده‌ی از آن مشکلی نخواهد داشت:
let z: number | undefined;
console.log(z);

یک نکته: خواص و پارامترهای اختیاری، به صورت خودکار دارای نوع undefined نیز هستند. برای مثال امضای متد ذیل:
method1(x?: number) {
}
با متد زیر یکی است:
method1(x?: number | undefined) {
}


اجبار به بررسی نال نبودن مقادیر، پیش از استفاده‌ی از آن‌ها در متدهای نال نپذیر

اگر پارامتر متدی یا خاصیت شیءایی نال پذیر نباشند، با ارسال مقدار نوعی به آن‌ها که می‌تواند null و یا undefined را بپذیرد، یک خطای زمان کامپایل صادر خواهد شد. در اینجا محافظ‌های نوع‌ها توسعه یافته‌اند تا اگر بررسی نال یا undefined بودن مقداری انجام شد، مشکلی در جهت استفاده‌ی از آن‌ها نباشد:
  f(x: number): string {
    return x.toString();
  }

  testTypeGuards() {
    let x: number | null | undefined;
    if (x) {
      this.f(x);  // Ok, type of x is number here
    } else {
      this.f(x);  // Error, type of x is number? here
    }
  }
در این مثال، متد f فقط یک عدد را می‌پذیرد (و نه نال و یا undefined). اما در حین کاربرد آن در متد testTypeGuards، مقدار متغیر x می‌تواند یک عدد، نال و یا undefined باشد. چون پیش از اولین استفاده‌ی از متد f در اینجا، بررسی دارای مقدار بودن این متغیر صورت گرفته‌است، فراخوانی صورت گرفته، مجاز است. اما در قسمت else این شرط، کامپایلر خطای ذیل را صادر می‌کند:
 Argument of type 'number | null | undefined' is not assignable to parameter of type 'number'.
Type 'undefined' is not assignable to type 'number'.

امکان این بررسی در مورد عبارات شرطی نیز صادق است:
getLength(s: string | null) {
   return s ? s.length : 0;
}


توسعه‌ی محافظ‌های نوع‌ها جهت کار با نوع‌های نال نپذیر

در مثال ذیل، خروجی متد isNumber دارای امضایی به همراه is است:
isNumber(n: any): n is number { // type guard
   return typeof n === "number";
}
به یک چنین متدهایی type guard گفته می‌شود که امکان بررسی یک نوع را میسر می‌کنند. از این امکان می‌توان جهت بررسی بهتر پارامترها و یا خواص اختیاری استفاده کرد:
  usedMb(usedBytes?: number): number | undefined {
    return this.isNumber(usedBytes) ? (usedBytes / (1024 * 1024)) : undefined;
  }
یک چنین بررسی، بهتر است از بررسی ذیل:
  usedMb2(usedBytes?: number): number | undefined {
    return usedBytes ? (usedBytes / (1024 * 1024)) : undefined;
  }
از این جهت که عبارت شرطی بررسی شده، مقدار صفر را نیز به صورت undefined بازگشت خواهد داد (if(0) به false تعبیر می‌شود و قسمت else این شرط فراخوانی خواهد شد).
همچنین امضای متد نیز به number | undefined تغییر یافته‌است. در غیر اینصورت، خطای زمان کامپایل Type undefined is not assignable to type number صادر خواهد شد.
در حین استفاده‌ی از یک چنین متدی، دیگر نمی‌توان به خروجی آن به صورت ذیل دسترسی یافت:
  formatUsedMb(): string {
    //ERROR: TS2531: Object is possibly undefined
    return this.usedMb(123).toFixed(0).toString();
  }
چون مقدار usedMb می‌تواند undefined باشد، باید ابتدا آن‌را بررسی کرد:
  formatUsed(): string {
    const usedMb = this.usedMb(123);
    return usedMb ? usedMb.toFixed(0).toString() : "";
  }


لغو بررسی strictNullChecks به صورت موقت

با استفاده از اپراتور ! می‌توان به کامپایلر اطمینان داد که این متغیر یا خاصیت، دارای مقدار نال نیست و نخواهد بود:
export interface User {
  name: string;
  age?: number;
}
در این اینترفیس، خاصیت age به صورت اختیاری تعریف شده‌است. برای نمایش مقدار age با فعال بودن strictNullChecks، یا باید ابتدا null نبودن آن‌را به صورت صریحی بررسی کرد:
  printUserInfo(user: User) {
    if (user.age != null) {
      console.log(`${user.name}, ${user.age.toString()}`);
    }
  }
در غیراینصورت قطعه کد ذیل با خطای 'Object is possibly 'undefined کامپایل نخواهد شد:
  printUserInfo(user: User) {
    console.log(`${user.name}, ${user.age.toString()}`);
  }

و یا می‌توان توسط اپراتور ! این بررسی را به صورت موقت خاموش کرد:
  printUserInfo(user: User) {
    console.log(`${user.name}, ${user.age!.toString()}`);
  }
البته استفاده‌ی از این اپراتور توسط tslint توصیه نمی‌شود:
 [tslint] Forbidden non null assertion (no-non-null-assertion)
چون بهتر است به کامپایلر عنوان نکنیم «قسم می‌خورم که این مقدار نال نیست»!



یک نکته‌ی تکمیلی
پس از آزمایش موفقیت آمیز نوع‌های نال نپذیر در TypeScript، مایکروسافت قصد دارد این ویژگی را به C# 8.0 نیز در مورد نوع‌های ارجاعی که می‌توانند نال پذیر باشند، اضافه کند (امکان داشتن نوع‌های ارجاعی نال‌نپذیر).
  • #
    ‫۶ سال و ۷ ماه قبل، چهارشنبه ۲۵ بهمن ۱۳۹۶، ساعت ۱۳:۰۹
    یک نکته‌ی تکمیلی: اضافه شدن strictPropertyInitialization به TypeScript 2.7

    در نگارش 2.7 اگر یک چنین تعریفی را داشته باشید:
    export class MovieComponent {
    
      @Input() movie: Movie;
    
    }
    خطای زیر را دریافت خواهید کرد:
    Error! Property movie has no initializer and is not assigned directly in the constructor.
    یک روش برطرف کردن این خطا، نال‌پذیر تعریف کردن این خاصیت و سپس مقدار دهی اولیه‌ی به آن است:
    @Input() movie: Movie | null = null;
    و یا در مثال زیر می‌توان مشخص کرد که baz می‌تواند undefined هم باشد:
    class C {
        baz: boolean | undefined;
    }
    روش دیگر، مقدار دهی اولیه، حین تعریف یک خاصیت است:
    class C {
        bar = "hello";
    }
    و یا می‌توان این بررسی را به صورت زیر خاموش کرد:
    class C {
        foo!: number;
    
        ngOnInit() {
            this.foo = 0;
        }
    }
    با استفاده از عملگر ! به کامپایلر اعلام می‌کنیم که این خاصیت حتما قرار است توسط روشی کمکی مقدار دهی اولیه شود و نال یا بدون مقدار نیست و استفاده‌ی از آن در این کلاس امن است.