22 января 2022

⚠️ Как не нужно учить TypeScript: 5 распространенных ошибок

Frontend-разработчик в Foquz. https://www.cat-in-web.ru/
Изучить TypeScript не так просто, как кажется, и новички легко сбиваются с правильного пути. Рассматриваем пять самых популярных ошибок начинающих разработчиков.
⚠️ Как не нужно учить TypeScript: 5 распространенных ошибок

«Мы с TypeScript никогда не найдем общий язык». Признавайтесь, вы порой тоже так думаете? Если да, то эта статья для вас.

Изучение TypeScript даже сейчас, в 2022 году, может быть сложным делом – по многим причинам. Например, программисты, имеющие опыт работы с Java или C#, будут обескуражены тем, что вещи работают не так, как ожидается. А разработчики, привыкшие к старому доброму JS, могут вступить в неравную битву с компилятором, который ругается на, казалось бы, нормальный код.

Мы собрали несколько ошибок, которые новички в TypeScript совершают особенно часто. Может быть, их опыт поможет вам избежать некоторых граблей. Предупрежден, значит, вооружен!

Ошибка #1. Игнорировать JavaScript

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

Возьмем для примера простой кейс с обработкой ошибок:

        try {
  // какой-то код, использующий, например, Axios
} catch(e: AxiosError) {
  // ^^^^^^^^^^ Error 1196 💥
}
    

Этот код кажется вполне разумным, и в других языках программирования он будет работать так, как ожидается. Но не в JavaScript. И даже не в TypeScript. Дело тут в способе обработки ошибок, который используется под капотом (подробно по ссылке выше). Не всегда код, имеющий смысл, является жизнеспособным.

Другой пример: использование метода Object.keys. Вы, вероятно, ожидаете, что сможете легко получить доступ к свойствам объекта по ключам, однако у меня для вас плохие новости:

        type Person = {
 name: string, age: number, id: number,
}

declare const me: Person;

Object.keys(me).forEach(key => {
 // 💥 Эта строка вызовет ошибку
 console.log(me[key])
})
    

Есть способ пофиксить это ненормальное поведение, но его можно применить не для всех сценариев. TypeScript просто не может гарантировать, основываясь на вашем коде, что типы для доступа к этому свойству будут именно теми, какие вы ожидаете.

Этот код прекрасно работает в JavaScript, но по многим причинам его очень трудно выразить в рамках системы типов.

Если вы изучаете TypeScript, не имея вообще никакого JS-бэкграунда, вы должны научиться разделять сам язык и привнесенную в него систему типов. Типы лишь часть вашего кода. Они выражают намерения, а вот реальные действия выполняет JavaScript, и в нем, на самом деле, тоже очень много хорошего. Вам стоит узнать его поближе.

Недавно на сайте TypeScript появилась фраза, которая максимально точно описывает этот проект:

TypeScript – это JavaScript с синтаксисом для типов.

Понимание JavaScript – это ключ к пониманию TypeScript.

Ошибка #2. Добавлять аннотации ко всему подряд

Аннотации типов – это способ явно указать, какие типы данных код ожидает получить или собирается вернуть. В ряде языков программирования вы должны писать многословные инструкции типа StringBuilder stringBuilder = new StringBuilder(), чтобы быть уверенным, что вы точно имеете дело со StringBuilder. Примерно то же самое можно делать в TypeScript. Или можно обойтись вообще без аннотаций и заставить компилятор определять типы за вас, во многих случаях он с этим справится.

        let a_number = 2; // тип number
    

Аннотации типов – это самое очевидное различие между TypeScript и JavaScript, и новички часто эксплуатируют его по полной, добавляя везде, где только можно. Ведь кажется очевидным, что нужно явно обозначать все типы, с которыми вы работаете – а иначе зачем нам вообще TypeScript?

Но все же постарайтесь себя контролировать и позвольте TypeScript определять типы за вас. Давайте разберемся, что такое аннотация на самом деле.

Аннотация типа – это способ указать, где необходимо проверять соглашения о типах.

Добавляя аннотацию к объявлению переменной, вы просите компилятор проверить, совпадают ли типы, во время присваивания.

        type Person = {
 name: string,
 age: number
}

const me: Person = createPerson()
    

Если createPerson вернет что-то непохожее на Person, TypeScript выдаст ошибку. Если вы действительно хотите быть уверены, что имеете дело с правильным типом, добавьте здесь аннотацию.

С этого момента переменная me относится к типу Person. Если она содержит дополнительные свойства помимо name и age, например, profession, TypeScript не позволит вам получить к ним доступ, так как тип Person этого не предполагает.

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

        function createPerson(): Person {
 return { name: "Stefan", age: 39 }
}
    

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

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

        function printPerson(person: Person) {
 console.log(person.name, person.age)
}

printPerson(me)

    

Это, вероятно, самая важная аннотация, от которой вы никуда не денетесь. Но все остальное можно вывести самостоятельно.

        type Person = {
 name: string,
 age: number
}

// тип возвращаемого значения - { name: string, age: number }
function createPerson() {
 return { name: "Stefan", age: 39}
}

// тип переменной me - { name: string, age: number}
const me = createPerson()

// Аннотация! Необходимо проверить соответствие типов
function printPerson(person: Person) {
 console.log(person.name, person.age)
}

// Все работает
printPerson(me)
    

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

        type Person = {
 name: string,
 age: number
}

type Studying = {
 semester: number
}

type Student = {
 id: string,
 age: number,
 semester: number
}

function createPerson() {
 return { name: "Stefan", age: 39, semester: 25, id: "XPA"}
}

function printPerson(person: Person) {
 console.log(person.name, person.age)
}

function studyForAnotherSemester(student: Studying) {
 student.semester++
}

function isLongTimeStudent(student: Student) {
 return student.age - student.semester / 2 > 30 && student.semester > 20
}

const me = createPerson()

// Все работает!
printPerson(me)
studyForAnotherSemester(me)
isLongTimeStudent(me)
    

Типы Student, Person и Studying имеют некоторые пересечения, но не связаны друг с другом. Функция createPerson возвращает значение, которое совместимо со всеми тремя типами. А если бы мы дали себе волю и добавили больше аннотаций, то пришлось бы создавать больше типов и выполнять без очевидной необходимости больше проверок.

Изучая TypeScript без злоупотребления аннотациями, вы получите хорошее представление о работе со структурной системой типов.

Больше полезной информации вы можете найти на нашем телеграм-канале «Библиотека фронтендера»

Ошибка #3. Принимать типы за значения

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

Очень удобно считать, что TypeScript – это дополнительный слой типов на обычном JavaScript. Тонкий слой метаинформации, который будет удален еще до запуска кода в какой-либо среде выполнения.

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

        // TypeScript --> тип
type Collection<T> = {
 entries: T[]
}

// JavaScript --> значение
function printCollection(coll: Collection<unknown>) {
 console.log(...coll.entries)
}

    

Мы можем также назвать эти элементы типами (TS) или значениями (JS). Слой типов находится поверх слоя значений, поэтому мы можем использовать значения в слое типов, но не наоборот. У нас есть специальные ключевые слова для этого:

        // значение
const person = {
 name: "Stefan"
}

// тип
type Person = typeof person;
    

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

Очень раздражает, когда есть декларации, которые создают и типы, и значения. Например, классы (class) могут использоваться и в TypeScript в качестве типа, и в JavaScript как значение.

        // объявление
class Person {
 name: string

 constructor(n: string) {
   this.name = n
 }
}

// значение
const person = new Person("Stefan")

// тип
type PersonCollection = Collection<Person>

function printPersons(coll: PersonCollection) {
 //...
}
    

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

Если вы привыкли использовать такие элементы и как типы, и как значения, то можете всерьез задуматься, получив старую добрую ошибку TS2749: ‘YourType’ refers to a value, but is being used as a type.

        type PersonProps = {
 name: string
}

function Person({ name }: PersonProps) {
 return <p>{name}</p>
}

type Collection<T> = {
 entries: T
}

type PrintComponentProps = {
 collection: Collection<Person> // ERROR!
 // 'Person' refers to a value, but is being used as a type
}

    

Вот тут TypeScript реально сбивает с толку. Что такое тип, что такое значение, почему мы должны разделять их, почему это работает не так, как в других языках программирования. (Спойлер: классы в JavaScript на самом деле создают не один, а сразу два типа).

Чтобы не ломать голову, лучше сразу разобраться, где у нас типы, а где значения – и где границы, за которые не стоит заходить. Вот маленькая, но полезная табличка:

Способ объявления Тип Значение
Class + +
Enum + +
Interface +
Type Alias +
Function +
Variable +

При изучении TypeScript стоит сфокусироваться на функциях, переменных и простых псевдонимах типов (или интерфейсах, если они вам ближе). Так вы сможете быстро разобраться, что на каком уровне происходит.

Ошибка #4. Сразу идти ва-банк

Мы уже поговорили о том, какие ошибки совершают в TypeScript люди, которые пришли из других языков программирования. Но есть и другой распространенный кейс: опытные разработчики на JavaScript.

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

Предполагается, что TypeScript помогает разработчику быть более продуктивным, но кажется, что он только отвлекает вас этими красными закорючками. Как это вообще может кому-то нравиться?

Вы проходили эту стадию?

Действительно, TypeScript может быть очень надоедливым, если вы «просто добавите его» в существующий JavaScript-проект. Он захочет получить представление обо всем вашем приложении и потребует аннотаций и соблюдения соглашений – очень много дополнительной работы.

Если вы пришли из в TypeScript из JavaScript, вам следует внедрять его постепенно, а не идти сразу ва-банк. И для этого есть все возможности.

Выберите небольшую часть вашего приложения и переведите ее на TypeScript, чтобы не испытывать сильного шока. Помните, что TypeScript полностью совместим с JavaScript (allowJs) и может выдавать скомпилированный JS-код даже при наличии ошибок типов. Для этого явно отключите параметр noEmitOnError. Так вы сможете не останавливать работу, даже если компилятор на вас ругается. Ознакомьтесь с другими настройками в документации TypeScript.

Создайте файлы с объявлениями типов и импортируйте их с помощью JSDoc. Это хорошее начало для получения дополнительной информации о том, что происходит внутри вашей кодовой базы. Вы можете использовать столько типов, сколько нужно. Кроме того, вопреки популярному мнению, использовать any абсолютно нормально, если вы делаете это явно.

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

Ошибка #5. Изучать неправильный TypeScript

Если вы обнаружили, что используете в вашем коде какие-то слова из списка ниже, то, вероятно, вы изучаете не ту часть TypeScript, которую следует:

  1. namespace
  2. declare
  3. module
  4. <reference>
  5. abstract
  6. unique

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

***

Расскажите о вашем опыте изучения TypeScript! Какие ошибки вы совершали, с какими сложностями вы столкнулись и как преодолели их?

Материалы по теме

Источники

Комментарии

ВАКАНСИИ

Добавить вакансию
Разработчик C++
Москва, по итогам собеседования

ЛУЧШИЕ СТАТЬИ ПО ТЕМЕ