Осваиваем TypeScript: 21 лучшая практика при написании кода

Kate

Administrator
Команда форума
TypeScript является популярным открытым языком программирования, идеально подходящим для современной разработки. За счёт своей продвинутой системы типов он позволяет писать более надёжный, обслуживаемый и масштабируемый код. Однако, чтобы задействовать весь потенциал этого языка и создавать высококачественные проекты, важно понимать лучшие практики и следовать им.

В этой статье мы углубимся в мир TypeScript и изучим 21 лучшую практику, с помощью которых вы сможете повысить свой навык работы с этим языком. Эти практики охватывают широкий спектр тем и сопровождаются конкретными вариантами применения в реальных проектах. Независимо от того, являетесь ли вы начинающим либо опытным разработчиком на TS, эта статья даст вам ценное понимание и рекомендации, которые помогут писать более чистый и эффективный код.

▍ Лучшая практика 1: строгая проверка типов​


Начнём с самых азов. Представьте, что имеете возможность перехватывать потенциальные ошибки ещё до их возникновения. Звучит слишком хорошо, чтобы быть правдой? Но именно это позволяет вам получить строгая проверка типов в TS. Эта лучшая практика связана с перехватом тонких багов, которые тихо просачиваются в код и вызывают проблемы в дальнейшем.

Строгая проверка типов позволяет обеспечить, чтобы все переменные имели те типы, которые вы для них задумали. К примеру, это означает, что в случае объявления переменной с типом string TS убедится, чтобы присвоенное ей значение действительно было строкой, а не числом. Это помогает отлавливать ошибки на ранних стадиях и добиваться должной работы кода.

Включить режим строгой проверки типов можно простой установкой параметра “strict”: true в файле tsconfig.json (по умолчанию должен быть true). В результате TS активирует набор проверок, перехватывающих определённые ошибки, которые в противном случае, остались бы незамеченными.

Вот пример того, как проверка типов может избавить вас от распространённой ошибки:

let userName: string = "John";
userName = 123; // TypeScript выбросит исключение, поскольку "123" не является строкой.

▍ Лучшая практика 2: вывод типов​


TypeScript построен вокруг конкретного обозначения типов, но это не значит, что вы обязаны все определять явно.

Вывод типов – это возможность компилятора TS автоматически определять тип переменной на основе присвоенного ей значения. Это означает, что вам не нужно явно указывать тип переменной при каждом её объявлении. Вместо этого компилятор будет анализировать присвоенное ей значение и выводить соответствующий тип.

Например, в следующем фрагменте кода TS автоматически выведет тип name как string:

let name = "John";

Эта возможность особенно полезна при работе со сложными типами либо при инициализации переменной со значением, возвращаемым функцией.

Но здесь нужно помнить, что вывод типов – это не волшебная палочка, и иногда лучше указывать типы явно, особенно при работе с их сложными вариантами либо в случаях, когда требуется использовать определённый тип.

▍ Лучшая практика 3: линтеры​


Линтеры – это инструменты, которые помогают улучшить код за счёт его проверки на соответствие набору правил и руководств.

Для TS существует несколько линтеров вроде TSLint и ESLint, которые помогают добиться согласованности стиля кода и перехватить потенциальные ошибки. Эти линтеры можно настроить на обнаружение таких ошибок, как упущенные точки с запятой, неиспользуемые переменные и не только.

▍ Лучшая практика 4: интерфейсы​


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

Интерфейс в TS определяет контракт для формы объекта. Он указывает свойства и методы, которыми объект данного типа должен обладать, и может использоваться в качестве типа для переменной. Это означает, что в случае присваивания объекта переменной с типом interface TS будет проверять, чтобы этот объект имел все указанные в данном интерфейсе свойства и методы.

Вот пример определения и использования интерфейса:

interface User {
name: string;
age: number;
}
let user: User = {name: "John", age: 25};

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

▍ Лучшая практика 5: псевдонимы типов (type alias)​


TypeScript позволяет создавать кастомные типы, используя так называемые псевдонимы типов. Основное различие между ними и интерфейсами в том, что при использовании псевдонима мы создаём новое имя для типа, а интерфейс создаёт новое имя для формы объекта.

Например, псевдоним типа можно использовать, чтобы создать собственный тип для точки в двухмерном пространстве:

type Point = { x: number, y: number };
let point: Point = { x: 0, y: 0 };

С помощью псевдонимов типов также можно создавать сложные типы вроде объединённого типа или типа пересечения.

type User = { name: string, age: number };
type Admin = { name: string, age: number, privileges: string[] };
type SuperUser = User & Admin;

▍ Лучшая практика 6: кортежи​


Кортежи позволяют создавать массив фиксированного размера с элементами разных типов, расположенных в определённом порядке.

К примеру, с помощью кортежа можно представить ту же точку в двухмерном пространстве:

let point: [number, number] = [1, 2];

С их помощью также можно представлять коллекцию элементов нескольких типов:

let user: [string, number, boolean] = ["Bob", 25, true];

Одно из основных свойств кортежей в том, что они дают возможность выразить конкретную связь между элементами коллекции и типами.

Помимо этого, с помощью деструктурирующего присваивания можно извлекать элементы кортежа и присваивать их переменным:

let point: [number, number] = [1, 2];
let [x, y] = point;
console.log(x, y);

▍ Лучшая практика 7: тип any​


Иногда у нас нет всей информации о типе переменной, но использовать её в коде всё же нужно. В подобных случаях можно задействовать тип any. Однако, как и в случае с любым мощным инструментом, при использовании any необходимо быть очень осторожным.

Один из лучших приёмов тут заключается в ограничении применения any до конкретных случаев, в которых тип действительно неизвестен. Это бывает при работе со сторонними библиотеками или динамически генерируемыми данными. Кроме того, будет нелишним добавлять утверждения типов или тайп гарды (type guard), гарантируя правильное использование переменной. Также по возможности старайтесь максимально сузить тип переменной.

Например:

function logData(data: any) {
console.log(data);
}

const user = { name: "John", age: 30 };
const numbers = [1, 2, 3];

logData(user); // { name: "John", age: 30 }
logData(numbers); // [1, 2, 3]

Ещё один полезный приём заключается в избегании использования any для аргументов функций и возвращаемых ей типов, поскольку он может понизить общую безопасность типов кода. Вместо этого рекомендуется использовать более конкретный тип либо более обобщённый вроде unknown или object, который обеспечит хоть какой-то уровень безопасности типов.

▍ Лучшая практика 8: тип unknown​


Unknown – это рестриктивный тип, который был введён в TypeScript 3.0. Он более ограничен в сравнении с any и может избавить вас от ряда непредвиденных ошибок.

В отличие от any при использовании типа unknown компилятор не даст выполнить какую-либо операцию для значения без предварительной проверки его типа. Это позволит перехватить ошибки типов во время компиляции, а не в среде выполнения.

К примеру, тип unknown можно использовать для создания более типобезопасной функции:

function printValue(value: unknown) {
if (typeof value === "string") {
console.log(value);
} else {
console.log("Not a string");
}
}

С его помощью также можно создавать более типобезопасные переменные:

let value: unknown = "hello";
let str: string = value; // Ошибка: тип 'unknown' нельзя присвоить типу 'string'.

▍ Лучшая практика 9: тип Object​


Тип Object является встроенной возможностью TypeScript, позволяющей ссылаться на базовый тип объекта. С его помощью можно повысить типобезопасность кода, обеспечив наличие у всех объектов определённых свойств или методов.

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

function printObject(obj: Object) {
console.log(obj);
}

Тип Object также можно использовать для создания более типобезопасных переменных:

let obj: Object = { name: "John", age: 30 };
let str: string = obj.name; // валидно
let num: number = obj.age; // валидно

С помощью него вы можете обеспечить, чтобы все объекты обладали определёнными свойствами или методами, повысив тем самым типобезопасность кода.

▍ Лучшая практика 10: тип never​


В TypeScript never является особым типом, представляющим значение, которое никогда не появится. С его помощью указывают, что функция вместо стандартного возвращения будет выбрасывать ошибку. Это отличный способ, позволяющий показать другим разработчикам (и компилятору), что функцию нельзя использовать определённым образом.

Рассмотрите, к примеру, следующую функцию, выбрасывающую ошибку при знаменателе равном 0:

function divide(numerator: number, denominator: number): number {
if (denominator === 0) {
throw new Error("Cannot divide by zero");
}
return numerator / denominator;
}

Здесь функция divide объявлена как возвращающая число, но если знаменатель равен нулю, она выбрасывает ошибку. Чтобы обозначить невозможность в таком случае выполнить возврат, можно использовать в качестве возвращаемого типа never:

function divide(numerator: number, denominator: number): number | never {
if (denominator === 0) {
throw new Error("Cannot divide by zero");
}
return numerator / denominator;
}

▍ Лучшая практика 11: оператор keyof​


Оператор keyof является эффективной возможностью TypeScript, позволяющей создавать тип, представляющий ключи объекта. С его помощью можно прояснять, какие свойства являются для объекта допустимыми.

К примеру, keyof можно использовать для создания более читаемого и обслуживаемого типа объекта:

interface User {
name: string;
age: number;
}
type UserKeys = keyof User; // "name" | "age"

С его помощью также можно создавать более типобезопасные функции, получающие в качестве аргументов объект и ключ:

function getValue<T, K extends keyof T>(obj: T, key: K) {
return obj[key];
}
let user: User = { name: "John", age: 30 };
console.log(getValue(user, "name")); // "John"
console.log(getValue(user, "gender")); // Ошибка: аргумент с типом '"gender"' нельзя присвоить параметру с типом '"name" | "age"'.

▍ Лучшая практика 12: Enums​


Enums – это перечисления, позволяющие определять в TS набор именованных констант. С их помощью можно писать более читаемый и обслуживаемый код, давая набору связанных значений имя, отражающее общий смысл.

К примеру, с помощью enum можно определить набор возможных значений статуса заказа:

enum OrderStatus {
Pending,
Processing,
Shipped,
Delivered,
Cancelled
}
let orderStatus: OrderStatus = OrderStatus.Pending;

Перечисления также могут иметь кастомный набор численных значений или строк.

enum OrderStatus {
Pending = 1,
Processing = 2,
Shipped = 3,
Delivered = 4,
Cancelled = 5
}
let orderStatus: OrderStatus = OrderStatus.Pending;

Соглашение об именовании требует называть перечисления с заглавной буквы и указывать их всегда в единственном числе.

▍ Лучшая практика 13: пространства имён​


Пространства имён позволяют организовывать код и избегать коллизий в именовании его элементов. С их помощью можно создать для кода контейнер, в котором определить переменные, классы, функции и интерфейсы.

Например, пространства имён можно использовать для группировки всего кода, связанного с определённым функционалом:

namespace OrderModule {
export class Order { /* … */ }
export function cancelOrder(order: Order) { /* … */ }
export function processOrder(order: Order) { /* … */ }
}
let order = new OrderModule.Order();
OrderModule.cancelOrder(order);

Пространства имён также позволяют предотвратить коллизии в именовании за счёт присваивания фрагменту кода уникального имени:

namespace MyCompany.MyModule {
export class MyClass { /* … */ }
}
let myClass = new MyCompany.MyModule.MyClass();

Важно отметить, что пространства имён аналогичны модулям, но используются для организации кода и избежания коллизий, в то время как модули служат для загрузки и выполнения кода.

▍ Лучшая практика 14: вспомогательные типы (utility types)​


Вспомогательные типы – это встроенная возможность TS, которая предоставляет набор готовых типов, позволяя писать более типобезопасный код. С их помощью можно более удобно выполнять стандартные операции над типами.

К примеру, вспомогательный тип Pick можно использовать для извлечения подмножества свойств из типа объекта:

type User = { name: string, age: number, email: string };
type UserInfo = Pick<User, "name" | "email">;
Exclude позволяет удалять из типа объекта свойства:
type User = { name: string, age: number, email: string };
type UserWithoutAge = Exclude<User, "age">;

Partial даёт возможность сделать все свойства типа необязательными:

type User = { name: string, age: number, email: string };
type PartialUser = Partial<User>;

▍ Лучшая практика 15: Readonly и ReadonlyArray​


При работе с данными в TypeScript иногда требуется обеспечить неизменность определённых значений, и здесь на помощь приходят ключевые слова Readonly и ReadonlyArray.

С помощью Readonly мы переводим свойства объекта в состояние «только для чтения», исключая возможность их изменения после создания. Такой приём пригождается, например, при работе с конфигурацией или постоянными значениями.

interface Point {
x: number;
y: number;
}
let point: Readonly<Point> = {x: 0, y: 0};
point.x = 1; // TypeScript выдаст ошибку, поскольку "point.x" является read-only

ReadonlyArray аналогично Readonly, но используется для массивов.

let numbers: ReadonlyArray<number> = [1, 2, 3];
numbers.push(4); // TypeScript выдаст ошибку, поскольку "numbers" является read-only

▍ Лучшая практика 16: type guards​


При работе со сложными типами бывает сложно отслеживать возможные вариации переменной. В этом случае у нас есть тайп-гарды, которые позволяют сузить тип переменной на основе определённых условий.

Вот пример использования тайп-гарда для проверки, является ли переменная числом:

function isNumber(x: any): x is number {
return typeof x === "number";
}
let value = 3;
if (isNumber(value)) {
value.toFixed(2); // Благодаря тайп-гарду TypeScript знает, что "value" является числом.
}

Тайп-гарды также можно использовать с операторами in, typeof и instanceof.

▍ Лучшая практика 17: обобщённые типы (generics)​


Дженерики позволяют писать более универсальный код, способный работать с любым типом. С их помощью можно написать одну функцию, класс либо интерфейс, работающие с несколькими типами.

К примеру, обобщённую функцию можно использовать для создания массива любого типа:

function createArray<T>(length: number, value: T): Array<T> {
let result = [];
for (let i = 0; i < length; i++) {
result = value;
}
return result;
}
let names = createArray<string>(3, "Bob");
let numbers = createArray<number>(3, 0);

С помощью дженериков также можно создать класс, способный работать с любым типом данных:

class GenericNumber<T> {
zeroValue: T;
add: (x: T, y: T) => T;
}
let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function(x, y) { return x + y; };

▍ Лучшая практика 18: ключевое слово infer​


Ключевое слово infer представляет собой продвинутую возможность TS, позволяет извлечь тип переменной в отдельный тип.

К примеру, с помощью infer можно создать более точный тип для функции, возвращающей массив конкретного типа:

type ArrayType<T> = T extends (infer U)[] ? U : never;
type MyArray = ArrayType<string[]>; // MyArray имеет тип string

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

type ObjectType<T> = T extends { [key: string]: infer U } ? U : never;
type MyObject = ObjectType<{ name: string, age: number }>; // MyObject имеет тип {name:string, age: number}

▍ Лучшая практика 19: условные типы​


С помощью условных типов можно создавать новые типы на основе условий других типов, выражая тем самым сложные отношения между ними.

К примеру, условный тип можно использовать для получения возвращаемого типа функции:

type ReturnType<T> = T extends (…args: any[]) => infer R ? R : any;
type R1 = ReturnType<() => string>; // string
type R2 = ReturnType<() => void>; // void

Помимо этого, с их помощью можно получать свойства типа объекта, отвечающие конкретному условию:

type PickProperties<T, U> = { [K in keyof T]: T[K] extends U ? K : never }[keyof T];
type P1 = PickProperties<{ a: number, b: string, c: boolean }, string | number>; // "a" | "b"

▍ Лучшая практика 20: отображённые типы​


Отображённые типы представляют способ создания новых типов на основе существующих путём применения ряда операций к их свойствам.

К примеру, с помощью отображённого типа можно получить новый тип, представляющий вариацию существующего, но уже только для чтения:

type Readonly<T> = { readonly [P in keyof T]: T[P] };
let obj: { a: number, b: string } = { a: 1, b: "hello" };
let readonlyObj: Readonly<typeof obj> = { a: 1, b: "hello" };

Они также позволяют создавать новый тип, представляющий опциональную версию существующего:

type Optional<T> = { [P in keyof T]?: T[P] };
let obj: { a: number, b: string } = { a: 1, b: "hello" };
let optionalObj: Optional<typeof obj> = { a: 1 };

Отображённые типы можно использовать по-разному: для создания новых, а также для добавления, удаления или изменения свойств имеющихся.

▍ Лучшая практика 21: декораторы​


Декораторы представляют способ внесения дополнительной функциональности в класс, метод или свойство при помощи простого синтаксиса. Они позволяют расширить поведение класса без изменения его реализации.

К примеру, с помощью декоратора можно добавить в метод логирование:

function logMethod(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
let originalMethod = descriptor.value;
descriptor.value = function(…args: any[]) {
console.log(`Calling ${propertyKey} with args: ${JSON.stringify(args)}`);
let result = originalMethod.apply(this, args);
console.log(`Called ${propertyKey}, result: ${result}`);
return result;
}
}
class Calculator {
@logMethod
add(x: number, y: number): number {
return x + y;
}
}

Декораторы также позволяют добавлять в класс, метод или свойство метаданные, которые могут использоваться в среде выполнения.

function setApiPath(path: string) {
return function (target: any) {
target.prototype.apiPath = path;
}
}
@setApiPath("/users")
class UserService {
// …
}
console.log(new UserService().apiPath); // "/users"

▍ Заключение​


Надеюсь, что эта статья оказалась для вас полезна и расширила ваше понимание того, как можно писать более чистый и эффективный код.

Помните, всё это лишь лучшие практики и руководства, а не жёсткие правила, поэтому в первую очередь здесь нужно опираться на собственное разумение и здравый смысл. Также важно учитывать, что по мере развития этого языка и появления в нём новых возможностей лучшие практики могут меняться, а значит — нужно быть готовым к освоению нового.

 
Сверху