TypeScript е мощен език за програмиране с отворен код, който е строг набор от JavaScript. Разработен и поддържан от Microsoft, TypeScript добавя опционално статично въвеждане и други функции към JavaScript, което го прави популярен избор за мащабни, сложни проекти. Някои разработчици обаче критикуват TypeScript за неговата крива на обучение и добавена сложност, докато други го похвалиха за подобряване на качеството на кода и ранно улавяне на грешки.

В тази статия ще разгледаме примери за това как ефективно да използваме TypeScript в обичайни сценарии, за да можем да приложим тези концепции към нашите бъдещи проекти. Основно ще се съсредоточим върху primitives, types, interfaces и enumns.

Примитиви, типове, интерфейси и енуми, какво представляват те?

Примитиви

В TypeScript примитивните типове са основните типове данни, които формират градивните елементи на езика. Те включват:

  1. число: за числови стойности (напр. 1, 3,14, -100)
  2. низ: за текстови стойности (напр. „здравей“, „свят“)
  3. булев: за стойности true/false
  4. null и undefined: за празни или несъществуващи стойности
  5. символ: нов тип данни, въведен в ECMAScript 6, използван за създаване на уникални идентификатори

Тези примитивни типове имат съответните JavaScript литерали и могат да се използват за деклариране на променливи и константи. Например:

let myName: string = "John";
let myAge: number = 30;
let isStudent: boolean = false;

Освен това операторът typeof в TypeScript връща типа на променлива, която може да бъде един от тези примитивни типове.

console.log(typeof "hello");  // Output: string
console.log(typeof 100);     // Output: number
console.log(typeof true);    // Output: boolean

В допълнение, TypeScript предоставя някои специални типове, като any и unknown.

  1. всеки: any може да бъде присвоено на всяка променлива и ще приеме any стойност и any тип,
  2. неизвестен: unknown е като any, но е по-ограничителен и може да се използва, когато искате да сте по-стриктни с проверката на типа

Типове и интерфейси?

Видове

Типовете в TypeScript се използват за указване на очаквания тип данни на променлива или функционален параметър. Те могат да бъдат или примитивни типове, като числа, низове и булеви стойности, или сложни типове, като масиви, обекти и дефинирани от потребителя типове. Типовете могат да се дефинират с помощта на ключовата дума type.

Въведете основите чрез пример

type Brand = "audi" | "ford"; // Value must be "audi" or "ford"

type Car = {
  owner: string;
  milage: number;
  brand: Brand;
}

Интерфейси

Интерфейсите осигуряват начин за описване на структурата на даден обект. Те определят договор за формата на даден обект, като уточняват свойствата и методите, които трябва да има. Интерфейсите могат да бъдат дефинирани с помощта на ключовата дума interface.

Основи на интерфейса чрез пример

interface ICar = {
  owner: string;
  milage: number;
  brand: Brand;
}

Според официалната документация на Typescript:

Почти всички характеристики на интерфейса са налични като тип, ключовата разлика е, че типът не може да бъде отворен отново, за да се добавят нови свойства срещу интерфейс, който винаги може да се разшири.

Можете да прочетете повече за разликите между types и interfaces в официалната документация на машинописа

Енуми

В TypeScript enum е начин за дефиниране на набор от именувани стойности, които представляват конкретен набор от константи.

Можете да дефинирате enum, като използвате ключовата дума enum, последвана от име за enum и набор от членове, оградени във фигурни скоби. Всеки член има име и свързана стойност, която може да бъде или числов литерал, или друг член enum. На първия член се присвоява стойността 0 по подразбиране, а на всеки следващ член се присвоява стойността на предишния член плюс 1, освен ако не е изрично указано.

enum EFuel {
  petrol,
  diesel,
}

Можете също изрично да зададете стойностите на членовете на enum:

enum EColor {
  "red" = "#ff0000",
  "blue" = "#0000ff",
}

Конвенции за именуване

Общи конвенции

Типовете, интерфейсите и enum-ите трябва да се дефинират с помощта на Cascal Pascal. Регистър Pascal, известен също като горна камилска буква, е конвенция за именуване, при която първата буква на всяка дума в името е главна и няма долна черта между думите.

Лични конвенции

Единствено / Множествено число

Като лично предпочитание предпочитам всички типове, интерфейси и енуми да бъдат именувани в единствено число.

const car: ICar = {/* values */} // Single car
const cars: ICar[] = {/* values */} // Multiple cars

Префикси

За яснота и лесна идентификация може да е полезно ясно да се разграничат типовете, интерфейсите и изброяванията, особено когато се импортират. За да подпомогна това разграничаване, приех конвенцията за префиксиране на интерфейсите с „I“ и enum с „E“, като оставям типовете без префикс. Това позволява бърза и лесна идентификация при навигация в кодовата база.

Различните разработчици имат свои собствени конвенции и са представили убедителни аргументи за и против различни подходи. В крайна сметка е важно да намерите подхода, който работи най-добре за вас и вашия екип, и да сте последователни в прилагането му. Това ще помогне да се гарантира, че вашият код е ясен, четим и поддържаем.

interface ICar = {
  owner: string;
  milage: number;
  brand: Brand;
}
enum EColor {
  "red" = "#ff0000",
  "blue" = "#0000ff",
}

Машинопис по пример

Сега, след като имаме основно разбиране, нека се потопим по-дълбоко в Typescript, като изследваме употребата му в общи сценарии, като използваме измислено приложение за продажба на автомобили като практически пример.

В нашето приложение всяка кола трябва да има 6 свойства:

  • owner
  • milage
  • fuel
  • color
  • brand
  • radioCode

Можем свободно да напишем нашия обект кола с примитиви:

export interface ICar {
  owner: string;
  milage: number;
  fuel: string;
  color: string;
  brand: string;
  radioCode: string;
}

Въпреки че този интерфейс е технически валиден, можем да подобрим неговата безопасност на типа, като добавим специфични типове към определени свойства, като fuel и brand. Нека да разгледаме по-отблизо как можем да постигнем това. Като се има предвид, че видовете и марките гориво са ограничени до конкретен набор от опции, можем да подобрим безопасността на типа, като използваме enums за тези свойства:

enum EFuel {
  petrol,
  diesel,
}

enum EBrand {
  audi,
  ford,
}

Боравенето с цветовете може да бъде малко по-сложно, тъй като те обикновено имат както разбираемо за човека име, така и HEX код. В този случай можем да създадем enum с двойки ключ-стойност, за да се справим с това. Това ни позволява да гарантираме, че работим само с валидни цветови опции, а също така ни дава гъвкавостта за лесно справяне и показване на името и кода на цвета.

enum EColor {
  "red" = "#ff0000",
  "blue" = "#0000ff",
}

Вече можем да препращаме към стойността на EFuel, EBrand и EColor, подобно на свойство на обект:

const fuel = EFuel.petrol; // Or EFuel["petrol"]
const brand = EBrand.audi; // Or EBrand["audi"]
const color = EColor.red; // Or EFuel["red"]

Можем също да създаваме типове от тези изброявания, като използваме ключовите думи keyof и typeof. Тези типове могат да се използват за по-добро дефиниране на нашия ICar интерфейс, замяна на примитиви, гарантирайки, че само валидни опции са присвоени на съответните свойства.:

type Fuel = keyof typeof EFuel; // "petrol" | "diesel"
type Brand = keyof typeof EBrand; // "audi" | "ford"
type Color = keyof typeof EColor; // "red" | "blue"

export interface ICar {
  owner: string;
  milage: number;
  fuel: Fuel;
  color: Color;
  brand: Brand;
  radioCode: string;
}

Подобно на enums, можем също да създаваме типове от справочни таблици, нека си представим в нашето измислено приложение, че имаме таблица за търсене на радиокодове:

const radioCodes = {
    sony: "1111",
    pioneer: "2222"
};

Точно като enum можем да създадем тип от нашата radioCodes справочна таблица, използвайки keyof и typeof ключовите думи:

type RadioCode = keyof typeof radioCodes; // "sony" | "pioneer"

Тип RadioCode може да се използва за по-добро дефиниране на свойство radioCodeв нашия ICar интерфейс, замествайки string, като се гарантира, че са присвоени само валидни опции:

export interface ICar {
  owner: string;
  milage: number;
  fuel: Fuel;
  color: Color;
  brand: Brand;
  radioCode: RadioCode;
}

Струва си да се отбележи, че в действителност не всички автомобили са оборудвани с радиостанции, което би оставило свойството radioCode недефинирано за тези автомобили, това би нарушило нашето приложение.
За да гарантираме, че нашето приложение не е повредено от наличието на недефинирани стойности за radioCode можем да го направим незадължителен, като свържем ? в края на името на ключа:

export interface ICar {
  owner: string;
  milage: number;
  fuel: Fuel;
  color: Color;
  brand: Brand;
  radioCode?: RadioCode;
}

В момента интерфейсът ICar в нашето приложение изисква string стойност за свойството owner. Нека обаче разгледаме възможността нашето приложение да има предварително дефиниран масив от собственици:

const people = ["Liam Hall", "Steve Jobs", "Ozzy Osborne", "Robert De Niro"];

Първо можем да напишем този масив, знаем, че всички стойности в масива трябва да са низове, така че добавяйки string примитива с [] към, за да го маркираме като масив от тип, можем да гарантираме, че const е строго въведен в масив от низове :

const people: string[] = ["Liam Hall", "Steve Jobs", "Ozzy Osborne", "Robert De Niro"];

С масива от people на място, сега можем да установим по-специфичен тип данни за свойството owner на нашия интерфейс ICar, което го прави по-стабилен, като гарантира, че само един от нашите предварително дефинирани хора може да бъде зададен като автомобили owner. За да направим това, използваме ключовата дума typeof:

type Person = typeof people[number];

Което вече можем да добавим към нашия ICar интерфейс:

export interface ICar {
  owner: Person;
  milage: number;
  fuel: Fuel;
  color: Color;
  brand: Brand;
  radioCode?: RadioCode;
}

В действителност свойството owner обикновено предоставя допълнителна информация. Освен това не е необичайно собственикът на автомобил да бъде представен като бизнес субект, като например гараж, а не като физическо лице. Основно изпълнение на тази концепция с помощта на интерфейси може да изглежда по следния начин:

export interface IPerson {
  firstName: string;
  lastName: string;
}

export interface IGarage {
  name: string;
  companyNumber: number
}

Използвайки тези интерфейси, нека създадем няколко нови масива, people и garages, заменяйки предишния масив от низове:

const people: IPerson[] = [
  {firstName: "Liam", lastName: "Hall"},
  {firstName: "Steve", lastName: "Jobs"},
  {firstName: "Ozzy", lastName: "Osborne"},
  {firstName: "Robert", lastName: "De Niro"}
];

const garages: IGarage[] = [
  {name: "London Garage", companyNumber: 1111},
  {name: "New York Garage", companyNumber: 2222},
  {name: "Tokyo Garage", companyNumber: 3333},
  {name: "Lagos Garage", companyNumber: 4444}
  {name: "Berlin Garage", companyNumber: 5555}
];

Сега, като използваме оператора or |, можем да актуализираме нашето свойство ICar owner, за да приема типове IPerson и IGarage:

export interface ICar {
  owner: IPerson | IGarage;
  milage: number;
  fuel: Fuel;
  color: Color;
  brand: Brand;
  radioCode?: RadioCode;
}

Можем също да използваме оператора or за създаване на масиви от смесен тип:

const owners: (IPerson | IGarage)[] = [...people, ...garages];

В случаите, когато комбинация от различни типове данни ще се използва често в цялото приложение, може да е полезно да се установи родителски тип за тях. Това ще осигури по-организиран и последователен подход за работа със смесените данни:

type Owner = IPerson | IGarage;

Което ще ни позволи да актуализираме ICar:

export interface ICar {
  owner: Owner;
  milage: number;
  fuel: Fuel;
  color: Color;
  brand: Brand;
  radioCode?: RadioCode;
}

В определени сценарии може да е необходимо да имате масив с фиксирана структура от типове данни, например, където индекс 0 трябва да бъде от типа IGarage интерфейс, а индекс 1 трябва да бъде от тип IPerson интерфейс. В такива ситуации можем да използваме кортежи, за да дефинираме специфичната структура на масива:

const lastSale: [IGarage, IPerson] = [
  {name: "London Garage", companyNumber: 1111},
  {firstName: "Liam", lastName: "Hall"}
];

В реалния свят колите могат да сменят собствениците си между физически лица и фирми, а в някои случаи може изобщо да не са били продадени. За да се справим с тази сложност, можем да използваме типа собственик, който създадохме преди това, и да използваме оператора or заедно с примитивния undefined:

const lastSale: [(Owner | undefined), Owner] = [
  {name: "London Garage", companyNumber: 1111},
  {firstName: "Liam", lastName: "Hall"}
];

Със стабилен, строго типизиран автомобилен интерфейс за нашето измислено приложение, нека проучим допълнителни типове, които биха могли да подобрят функционалността на нашето приложение.

Представете си, че дизайнерът на приложението е избрал да включи акцентни цветове, специфични за марката. За да постигнем това, трябва да установим съответствие между имената на марките и цветовете. За целта можем да създадем проста справочна таблица:

const brandAccents = {
  audi: "#000000",
  ford: "#333333"
}

Този подход ще функционира по предназначение, но в момента му липсва каквато и да е форма на безопасност на типа. За да гарантираме, че ключовете са изключително от типа „Марка“ и цветовете са последователно представени като низове, какви мерки можем да приложим? За да направим това, можем да използваме ключовите думи key in:

type BrandAccentMap = {
  [key in Brand]: string;
}

Вече можем да напишем brandAccents с BrandAccentMap:

const brandAccents: BrandAccentMap = {
  audi: "#000000",
  ford: "333333"
}

За да гарантираме, че всички ключове са от тип Brand, като същевременно позволяваме гъвкавост, че някои марки може да нямат акцентни цветове, можем да направим двойката ключ-стойност на марката незадължителна, като добавим ? към динамичния ключ, подобно на това, което направихме за radioCode в ICar:

type BrandAccentMap = {
  [key in Brand]?: string;
}

Разбира се, в реалния свят повечето приложения трябва да извличат данни от източник на данни. Нека разгледаме хипотетичен сценарий, при който нашето приложение изисква извличане на конкретно превозно средство от база данни, използвайки уникален идентификатор като id:

const getCar = (id) => {/* fetch data */}
// OR
// function getCar(id) {/* fetch data */}

Въпреки че тази функция е функционална, има достатъчно възможности за подобряване на безопасността на типа. Би било полезно ясно да се дефинират типовете както за аргумента, id, така и за изхода на функцията. В контекста на нашето хипотетично приложение аргументът id трябва да бъде посочен като string, а функцията трябва да върне object от тип ICar:

const getCar = (id: string): ICar => {/* fetch data */}
// OR
// function getCar(id: string): ICar {/* fetch data */}

Както е показано в примера, можем ясно да дефинираме типовете аргументи на функцията, като използваме двойка argument: type, разделени с двоеточие. В случай, че имаме няколко аргумента, можем да разделим двойките argument: type със запетаи.

В действителност процесът на получаване на данни в приложение често е асинхронен. За да отразим точно това в нашето въвеждане, можем да използваме типа Promise във връзка с типа ICar и да поставим префикс на функцията с ключовата дума async. Този подход гарантира, че типът връщане на нашата функция точно отразява асинхронния характер на извличането на данни:

const getCar = async (id: string): Promise<ICar> => {/* fetch data */}
// OR
// async function getCar(id: string): Promise<ICar> {/* fetch data */}

Понякога, когато извличаме данни, особено от системи за управление на съдържание, може да нямаме пълен контрол върху върнатата информация. В тези случаи данните, които желаем, могат да бъдат обвити с допълнителна, но полезна информация, предоставена от CMS, като например блок UUID. Върнатите данни може да изглеждат по следния начин:

{
  _uuid: "123",
  data: { /* primary data */ }
}

В ситуации като тази можем да използваме генерични средства, за да обвием нашите съществуващи типове и да посочим кое свойство трябва да бъде въведено като обвит тип. Например, използвайки гореспоменатия пример за двойки ключ-стойност _uuid и data, можем да създадем интерфейс ICMSResponse. Този подход ни позволява да поддържаме полезната информация, предоставена от CMS, като същевременно ясно дефинираме очакваната структура от данни на нашето приложение:

interface ICMSResponse<T> {
  _uuid: string,
  data: T,
}

T в ICMSResponse<T> представлява предаден тип, което показва, че свойството данни ще бъде въведено според типа, предаден на ICMSResponse. Това позволява гъвкавост и динамично писане, като същевременно поддържа ясна и последователна структура.

Ако очаквахме ICMSResponse от предишната ни функция getCar, бихме могли да актуализираме типовете на функцията, за да представят това:

export const getCar = async (id: string): Promise<ICMSResponse<ICar>> => { /* cms data */ }

Заключение

Надявам се, че тази статия ви е помогнала да придобиете по-задълбочено разбиране за това как ефективно да използвате TypeScript в различни често срещани сценарии. Като предоставям примери и практически обяснения, имам за цел да ви дам възможност уверено да включите TypeScript във вашите проекти за подобрена надеждност и четливост на кода.

Ако сте намерили тази статия за полезна, моля, последвайте ме в Medium, Dev.to и/или Twitter.

Повече съдържание в PlainEnglish.io. Регистрирайте се за нашия безплатен седмичен бюлетин. Следвайте ни в Twitter, LinkedIn, YouTube и Discord .

Интересувате ли се от мащабиране на стартирането на вашия софтуер? Вижте Circuit.

Допълнителна информация