نوع های آبجکت در تایپ اسکریپت [TypeScript]

نوع آبجکت در تایپ اسکریپت

در جاوا اسکریپت روش اساسی گروه بندی و انتقال داده ها، شیء یا object ها هستند. در تایپ اسکریپت هم برای توصیف آن ها از نوع شیء یا object type استفاده می کنیم.

این پنجمین مقاله از سری مقالات راهنمای جامع تایپ اسکریپت است. برای مشاهده بقیه بخش ها، فهرست مقالات را ببینید.

نوع آبجکت ها می توانند ناشناس (anonymous):

function greet(person: { name: string; age: number }) {
  return "Hello " + person.name;
}

یا نام گذاری شده باشند، از طریق interface:

interface Person {
  name: string;
  age: number;
}
 
function greet(person: Person) {
  return "Hello " + person.name;
}

و یا از طریق type alias:

type Person = {
  name: string;
  age: number;
};
 
function greet(person: Person) {
  return "Hello " + person.name;
}

ویژگی ها

هر ویژگی در یک نوع آبجکت می تواند چند چیز را مشخص کند: نوع ویژگی، اینکه آیا اختیاری است و اینکه آیا امکان اختصاص مقدار به آن وجود دارد یا نه.

ویژگی های اختیاری

برای توصیف نوع یک ویژگی اختیاری می توانیم از علامت سوال (?) استفاده کنیم:

interface PaintOptions {
  shape: Shape;
  xPos?: number;
  yPos?: number;
}
 
function paintShape(opts: PaintOptions) {
  // ...
}
 
const shape = getShape();
paintShape({ shape });
paintShape({ shape, xPos: 100 });
paintShape({ shape, yPos: 100 });
paintShape({ shape, xPos: 100, yPos: 100 });

در مثال بالا xPos و yPos اختیاری هستند و هنگام فراخوانی تابع paintShape می توانیم آنها را وارد نکنیم. ولی اگر وارد شوند، حتما باید نوع number را داشته باشند.

همچنین می توانیم به آنها دسترسی داشته باشیم ولی با فعال بودن strictNullChecks تایپ اسکریپت به ما می گوید که ممکن است undefined باشند.

function paintShape(opts: PaintOptions) {

  // xPos: number | undefined
  let xPos = opts.xPos;

  // yPos: number | undefined
  let yPos = opts.yPos;

  // ...
}

به این صورت با چک کردن undefined بودن آنها، می توانیم نوع محدودتر فقط number را داشته باشیم:

function paintShape(opts: PaintOptions) {

  // xPos: number
  let xPos = opts.xPos === undefined ? 0 : opts.xPos;

  // yPos: number
  let yPos = opts.yPos === undefined ? 0 : opts.yPos;

  // ...
}

همچنین می توانیم کد بالا را به این شکل هم با مشخص کردن مقدار پیش فرض برای پارامتر ها، بنویسیم:

function paintShape({ shape, xPos = 0, yPos = 0 }: PaintOptions) {

  console.log("x coordinate at", xPos); // xPos: number

  console.log("y coordinate at", yPos); // yPos: number

  // ...
}

در کد بالا ما آبجکت paintShape را destructure کردیم و مقادیر پیش فرضی به ویژگی های yPos و xPos دادیم که یعنی حالت undefined را از این دو ویژگی حذف کردیم و نوع آنها در داخل تابع فقط number شد.

توجه کنید که فعلا هیچ راهی برای مشخص کردن نوع داخل destructure کردن نیست، چون چنین عبارتی در جاوا اسکریپت معنی دیگری دارد:

function draw({ shape: Shape, xPos: number = 100 /*...*/ }) {
  render(shape); // خطا!
  render(xPos); // خطا!
}

در کد بالا shape: Shape یعنی اسم پارامتر shape در داخل تابع به Shape تغییر پیدا کرد و به همین شکل اسم xPos هم به number تغییر پیدا کرد. در اینجا number یک متغیر است نه نوع.

ویژگی های فقط خواندنی

همچنین ویژگی های آبجکت ها را می توانیم readonly تعریف کنیم که اجازه اختصاص و تغییر مقدار آن ویژگی را ندهیم. البته readonly فقط در سیستم بررسی نوع تاثیر می گذارد و در خود اجرای کد تاثیری ندارد:

interface SomeType {
  readonly prop: string;
}
 
function doSomething(obj: SomeType) {
  // می توانیم مقدارش را بخوانیم
  console.log(`prop has the value '${obj.prop}'.`);
 
  // خطا! نمی توانیم به آن مقدار اختصاص دهیم
  obj.prop = "hello";
}

همچنین readonly مقدار ویژگی را کلا غیر قابل ویرایش نمی کند و برای مثال ویژگی های داخلی آن قابل ویرایش هستند:

interface Home {
  readonly resident: { name: string; age: number };
}
 
function visitForBirthday(home: Home) {
  console.log(`Happy birthday ${home.resident.name}!`);
  // اوکی (ویژگی داخلی قابل تغییر است)
  home.resident.age++;
}
 
function evict(home: Home) {
  // نمی توانیم مستقیما خودش را تغییر دهیم
  home.resident = {
    name: "Victor the Evictor",
    age: 42,
  };
}

یا مثال دیگری از تغییر یک ویژگی readonly به صورت غیر مستقیم:

interface Person {
  name: string;
  age: number;
}
 
interface ReadonlyPerson {
  readonly name: string;
  readonly age: number;
}
 
let writablePerson: Person = {
  name: "Person McPersonface",
  age: 42,
};
 
// اوکی هست
let readonlyPerson: ReadonlyPerson = writablePerson;
 
console.log(readonlyPerson.age); // prints '42'
writablePerson.age++; // با این کار مقدارش در هر دو آبجکت تغییر می کند
console.log(readonlyPerson.age); // prints '43'

تعریف به روش ایندکس (Index Signatures)

گاهی اوقات نام ویژگی های یک آبجکت را نمی دانیم ولی نوع مقدارشان را می دانیم. در این صورت می توانیم آن ها را به روش ایندکس تعریف کنیم:

interface StringArray {
  [myIndex: number]: string;
}
 
const myArray: StringArray = ...;
const secondItem = myArray[1]; // secondItem: string

در مثال بالا [myIndex: number]: string; در اینترفیس StringArray یعنی یک آبجکت با ویژگی های عدد که مقدار آن ها رشته است (که می شود همان آرایه رشته ها).

فقط این از نوع ها را می توانیم به عنوان ایندکس انتخاب کنیم: string ، number ، symbol ، union ها ، template string و ترکیبات این ها.

وقتی یک آبجکت را به روش ایندکس تعریف می کنیم همه ویژگی های آن باید با ایندکس تعریف شده سازگار باشند. به مثال زیر توجه کنید:

interface NumberDictionary {
  [myProperty: string]: number;
 
  length: number; // ok
  name: string; // خطا!
}

در کد بالا چون تعریف کردیم همه ویژگی های NumberDictionary باید مقدارشان نوع number باشد، پس نمی توانیم ویژگی name را طوری تعریف کنیم که مقدارش string باشد.

البته می توانیم ایندکس را به این صورت تعریف کنیم که هم عدد و هم رشته را پوشش دهد:

interface NumberOrStringDictionary {
  [index: string]: number | string;
  length: number; // ok
  name: string; // ok
}

در نهایت می توانیم از readonly هم در تعریف ایندکس استفاده کنیم:

interface ReadonlyStringArray {
  readonly [theIndex: number]: string;
}
 
let myArray: ReadonlyStringArray = getReadOnlyStringArray();
myArray[2] = "Mallory"; // خطا!

گسترش (Extend) نوع ها

برای گسترش یا همان Extend کردن یک interface و اضافه کردن ویژگی های بیشتر به آن می توانیم از کلیدواژه extends به این صورت استفاده کنیم:

interface BasicAddress {
  name?: string;
  street: string;
  city: string;
  country: string;
  postalCode: string;
}
 
interface AddressWithUnit extends BasicAddress {
  unit: string;
}

همچنین با استفاده از extends می توانیم چندین اینترفیس را انتخاب کنیم:

interface Colorful {
  color: string;
}
 
interface Circle {
  radius: number;
}
 
interface ColorfulCircle extends Colorful, Circle {}
 
const cc: ColorfulCircle = {
  color: "red",
  radius: 42,
};

اشتراک نوع ها (Intersection)

روش دیگری که برای گسترش و استفاده مجدد از نوع های تعریف شده وجود دارد، اشتراک یا Intersection است. برای اشتراک دو نوع می توانیم آن ها را با استفاده از & به هم اضافه کنیم:

interface Colorful {
  color: string;
}
interface Circle {
  radius: number;
}
 
type ColorfulCircle = Colorful & Circle;

به صورت مستقیم بدون تعریف type هم می توانیم از & استفاده کنیم:

function draw(circle: Colorful & Circle) {
  console.log(`Color was ${circle.color}`);
  console.log(`Radius was ${circle.radius}`);
}
 
// okay
draw({ color: "blue", radius: 42 });
 
// خطا!
draw({ color: "red", raidus: 42 });

نوع های آبجکت جنریک

به مثال زیر توجه کنید:

interface Box {
  contents: any;
}

در حال حاضر نوع ویژگی contents برابر any است. با وجود اینکه این کد بدون خطا کار می کند ولی با فلسفه کلی استفاده از یک سیستم بررسی نوع، در تضاد است چون any در حقیقت سیستم بررسی نوع را غیرفعال می کند.

استفاده از unknown هم بی خطر نیست و مجبوریم یا صراحتا نوع دقیق را با جاوا اسکریپت چک کنیم و یا از as استفاده کنیم:

interface Box {
  contents: unknown;
}
 
let x: Box = {
  contents: "hello world",
};

if (typeof x.contents === "string") {
  console.log(x.contents.toLowerCase());
}

console.log((x.contents as string).toLowerCase());

البته یک راه بی خطر می تواند جداگانه تعریف کردن باشد:

interface NumberBox {
  contents: number;
}
 
interface StringBox {
  contents: string;
}
 
interface BooleanBox {
  contents: boolean;
}

ولی این یعنی زحمت بیشتر چون مجبوریم همه جا اورلود ها و عملیات های مختلفی برای هندل کردن آنها بنویسیم.

function setContents(box: StringBox, newContents: string): void;
function setContents(box: NumberBox, newContents: number): void;
function setContents(box: BooleanBox, newContents: boolean): void;
function setContents(box: { contents: any }, newContents: any) {
  box.contents = newContents;
}

با پیچیده شدن کد طبیعتا تعریف نوع های مجزا به صورت بالا بسیار سخت تر هم خواهد شد. بنابراین در تایپ اسکریپت نوع های آبجکت را هم می توانیم به صورت جنریک (Generic) تعریف کنیم:

interface Box<Type> {
  contents: Type;
}

و نحوه استفاده:

let box: Box<string>;

مثال دیگر:

interface Box<Type> {
  contents: Type;
}
 
interface Apple {
  // ....
}
 
// مثل این است که بنویسیم: '{ contents: Apple }'
type AppleBox = Box<Apple>;

در واقع می توانیم Box را یک قالب و تمپلیت برای نوع Type تصور کنیم. حتی می توانیم آن را با توابع جنریک هم ترکیب کرده و به کل از نوشتن اورلود های پیچیده دوری کنیم:

function setContents<Type>(box: Box<Type>, newContents: Type) {
  box.contents = newContents;
}

همچنین غیر از interface می توانیم type alias جنریک هم مثل نمونه های زیر تعریف کنیم:

type Box<Type> = {
  contents: Type;
};

type OrNull<Type> = Type | null;
 
type OneOrMany<Type> = Type | Type[];
 
type OneOrManyOrNull<Type> = OrNull<OneOrMany<Type>>;

type OneOrManyOrNullStrings = OneOrManyOrNull<string>;

نوع Array خود تایپ اسکریپت هم دقیقا یک نوع آبجکت جنریک است. یک آرایه ساختاری است که بدون وابستگی به نوع آیتم های داخلش، ویژگی ها و متد های مختلفی برای انجام عملیات مختلف دارد.

interface Array<Type> {
  length: number;

  pop(): Type | undefined;

  push(...items: Type[]): number;

  // ...
}

در جاوا اسکریپت موارد دیگری هم مثل Map<K, V> ، Set<T> و Promise<T> وجود دارند که در تایپ اسکریپت با جنریک ها تعریف شده اند.

نوع های آرایه فقط خواندنی

نوع ReadonlyArray نوع خاصی از همان نوع Array است که در آن اجازه استفاده از متد های تغییر دهنده آرایه را نداریم:

function doStuff(values: ReadonlyArray<string>) {
  // می توانیم مقدارش را بخوانیم
  const copy = values.slice();
  console.log(`The first value is ${values[0]}`);
 
  // خطا! نمی توانیم مقدارش را تغییر دهیم
  values.push("hello!");
}

البته این فقط یک نوع در تایپ اسکریپت و چنین چیزی در خود جاوا اسکریپت نداریم:

new ReadonlyArray("red", "green", "blue"); // خطا!

نوع های چندتایی (Tuple)

نوع tuple یا چندتایی یکی دیگر از نوع های آرایه است که تعداد و نوع دقیق تمام آیتم هایش را می داند:

type StringNumberPair = [string, number];

در کد بالا StringNumberPair دقیقا یک آرایه است که فقط دو آیتم دارد. نوع آیتم اول آن دقیقا string و نوع آیتم دوم number است. مثال استفاده:

function doSomething(pair: [string, number]) {
  const a = pair[0]; // a: string

  const b = pair[1]; // b: number

  const c = pair[2]; // خطا!
}
 
doSomething(["hello", 42]);

همچنین آخرین آیتم های یک نوع چندتایی می توانند اختیاری باشند که با ? مشخص می کنیم:

type Either2dOr3d = [number, number, number?];
 
function setCoordinate(coord: Either2dOr3d) {
  const [x, y, z] = coord;
  // x: number
  // y: number
  // z: number | undefined
  // coord.length: 2 | 3
}

همچنین می توانیم نوع چندتایی و آرایه را ترکیب کنیم و نوع های پیشرفته تری بسازیم:

type StringNumberBooleans = [string, number, ...boolean[]];
type StringBooleansNumber = [string, ...boolean[], number];
type BooleansStringNumber = [...boolean[], string, number];

و استفاده به این صورت:

const a: StringNumberBooleans = ["hello", 1];
const b: StringNumberBooleans = ["beautiful", 2, true];
const c: StringNumberBooleans = ["world", 3, true, false, true, false, true];

نوع های چندتایی فقط خواندنی

یک نوع چندتایی هم می تواند فقط خواندنی یا readonly باشد که اجازه اختصاص و تغییر مقادیرش را ندهد:

function doSomething(pair: readonly [string, number]) {
  pair[0] = "hello!"; // خطا!
}

قدم بعدی

در این مطلب با برخی از نوع های Object در تایپ اسکریپت آشنا شدیم. در ادامه این سری، ایجاد نوع از روی نوع های دیگر را با جزئیات بیشتری بررسی خواهیم کرد:

همچنین جهت یادگیری می توانید آموزش های پروژه محور مرتبط با تایپ اسکریپت را هم بررسی کنید.

برای یادگیری بهتر، علاوه بر مطالعه و تحقیق، سعی کنید تمرینات پروژه محور هم انجام دهید.

برای تست کد تایپ اسکریپت در مرورگر هم می توانید از TS Playground استفاده کنید.


کامنت ها