Как стать автором
Обновить

Как НЕ надо учить TypeScript

Время на прочтение9 мин
Количество просмотров33K
Автор оригинала: Stefan Baumgartner

Добрый день, меня зовут Павел Поляков, я Principal Engineer в каршеринг компании SHARE NOW, в Гамбурге в 🇩🇪 Германии. А еще я автор телеграм канала Хороший разработчик знает, где рассказываю обо всем, что обычно знает хороший разработчик.

Сегодня хочу поговорить про то как НЕ надо учить TypeScript. Какие ошибки чаще всего делают новички и почему TypeScript может так раздражать? Это перевод оригинальной статьи.

Как НЕ надо учить TypeScript

“TypeScript и я никогда не будут друзьями!”. Ох, как часто я слышал эту фразу? Учить TypeScript даже в 2022 году может быть сложно. И сложно может быть по множеству разных причин. Люди, которые пишут на Java или C# могут обнаружить вещи, которые работают не так, как должны. Люди, которые программировали на JavaScript большую часть своей жизни пугаются, когда на них кричит компилятор. Давайте рассмотрим некоторые ошибки, которые совершают люди, когда начинают знакомство с TypeScript. Надеюсь, они будут вам полезны!

Эта статья была вдохновлена статьей Denys “Как не надо учить Rust”, которую я тоже рекомендую прочитать.

Ошибка 1: игнорирование JavaScript

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

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

try {
	// something with Axios, for example
} catch (e: AxiosError) {
	//         ^^^^^^^^^^ Error 1196 💥
}

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

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

type Person = {
  name: string, age: number, id: number,
}
declare const me: Person;

Object.keys(me).forEach(key => {
  // 💥 the next line throws red squigglies at us
  console.log(me[key])
})

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

Если вы учите TypeScript, но никогда не сталкивались с JavaScript, то научитесь различать JavaScript и систему типов. Еще научитесь искать нужные вещи. Именованные параметры в функциях. Вы можете добиться этого передавая объект как аргумент. Это отличный паттерн, а еще это часть JavaScript. Оператор опциональной последовательности? Сначала реализован в компиляторе TypeScript, но тоже является свойством JavaScript. Классы и наследование? Это JavaScript. Приватные свойства? Ну вы знаете, те, которые начинаются с #, маленького забора, чтобы никто не получил доступ за него. Это тоже JavaScript.

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

Недавно, на сайте TypeScript появилось более четкое сообщение, о том, что значит использовать TypeScript: TypeScript это JavaScript с синтаксисом для типов. Вот оно, видели? TypeScript это и есть JavaScript. Понимание JavaScript является ключом к пониманию TypeScript.

Ошибка 2: аннотации везде

Аннотация типа это способ в явном виде указать какой тип стоит ожидать. Понимаете? Тот самый случай, когда в других языках, когда вы очень подробно пишитеStringBuilder stringBuilder = new StringBuilder() и понимаете, что это точно что-то, что имеет тип StringBuilder. Противоположностью является интерфейс типа, когда TypeScript пытается понять что это за тип за вас. let number = 2 имеет тип number.

Аннотации типа являются наиболее очевидным и видимым различием в синтаксисе между TypeScript и JavaScript.

Когда вы начинаете учить TypeScript, вы наверное захотите аннотировать все, чтобы выразить типы, которые вы ожидаете. Вы можете думать — TypeScript ведь для этого и создан! Но я прошу вас, используйте аннотации экономно, дайте TypeScript разобраться с этим за вас. Почему? Давайте я объясню чем на самом деле являются аннотации типов.

Аннотация типа это способ обозначить ГДЕ нужно проверить контракт. Когда вы добавляете аннотацию типа к объявлению переменной, то вы говорите компилятору проверить, есть ли соответствие типов во время присвоения.

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

const me: Person = createPerson()

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

Также, с этого момента me имеет тип Person и TypeScript будет относиться к этой переменной как к Person. Если в me есть дополнительные свойства, например, profession, TypeScript не разрешит вам обращаться к ним. Потому что они не определены в Person.

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

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

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

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

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

printPerson(me)

На мой взгляд это наиболее важный и неизбежный случай, когда надо применять аннотации типа. А вот во всех остальных случаях можно положиться на TypeScript, он сам определит тип (inferred type).

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

// Inferred!
// return type is { name: string, age: number }
function createPerson() {
	return {
		name: "Stefan",
		age: 39
	}
}

// Inferred!
// me is type of { name: string, age: number}
const me = createPerson()

// Annotated! You have to check if types are compatible
function printPerson(person: Person) {
	console.log(person.name, person.age)
}

// All works
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()

// All work!
printPerson(me)
studyForAnotherSemester(me)
isLongTimeStudent(me)

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

Когда вы учите TypeScript и не перебарщиваете с аннотациями типов, то вам легче работать с системой в которую интегрирована типизация (в отличие от JavaScript).

Ошибка 3: Путать типы и значения

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

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

То чтоTypeScript является лишь слоем над JavaScript так же значит, что определенный синтаксис оказывается влияние на определенные слои. Например, function или const создают сущность в JavaScript части, а type или interface создают сущность в слое TypeScript.

// Collection is in TypeScript land! --> type
type Collection < T > = {
	entries: T[]
}

// printCollection is in JavaScript land! --> value
function printCollection(coll: Collection < unknown > ) {
	console.log(...coll.entries)
}

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

// a value
const person = {
	name: "Stefan"
}

// a type
type Person = typeof person;

typeof создает сущность, которая доступна в слое, который отвечает за типы, на основе значения, которое обитает в слое пониже.

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

// declaration
class Person {
  name: string

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

// value
const person = new Person("Stefan")

// type
type PersonCollection = Collection<Person>

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

То, как названы сущности может сыграть с вами злую шутку. Обычно, мы определяем классы, типы, интерфейсы, enum-ы и т.п. с большой буквы. И даже если они могут создавать значения, они так же могут создавать типы. Ну...пока вы называете функции с большой буквы в вашем 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 по настоящему сбивает с толку. Где тип, где значение, почему мы должны это разделять, почему это не работает так, как в других языках программирования? Внезапно, вы обнаруживаете свой код под контролем typeof или даже хелпера InstanceType. Потом вы понимаете, что классы на самом деле могут влиять на два слоя — типы и значения (шок!).

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

Определение

Тип

Значение

Class

Enum

Interface

Type Alias

Function

Variable

Когда вы учите TypeScript хорошей идеей будет сосредоточиться на функциях, переменных и простых псевдонимах типов (type aliases). Ну или интерфейсах, если вы больше любите их. Это даст вам хорошее представление о том, что происходит в слое, который отвечает за типы, и что происходит в слое, который отвечает за значения.

Ошибка 4: Поставить все на TypeScript с самого начала

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

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

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

Мы все это испытывали, не правда ли?

Я тоже оказывался в подобной ситуации! TypeScript может быть слишком назойливым, особенно, если вы просто “подключили его” к существующему JavaScript проекту. TypeScript хочет понять как работает все ваше приложение, а это требует аннотировать все, чтобы контракты совпадали. В начале это может быть неудобно.

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

  1. Возьмите часть своего приложения и переведите ее на TypeScript, вместо того, чтобы включать его везде. TypeScript можно настроить работать вместе с JavaScript (allowJS)

  2. TypeScript выдает скомпилированный JavaScript код даже когда он видит ошибки. Вы должны отключить это вручную, используя флаг noEmitOnError. Тогда у вас останется старый код, даже когда компилятор кричит на вас.

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

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

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

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

Ошибка 5: Учить не тот TypeScript

Опять, этот пункт вдохновлен статьей “Как не надо учить Rust”. Если вашему коду нужны конструкции из списка ниже, вы, скорее всего, делаете что-то не то с TypeScript. Либо уже так продвинулись, что вам не нужно читать эту статью. Вот список:

  • namespace

  • declare

  • module

  • <reference>

  • abstract

  • unique

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

Вот и все. Мне интересно как вы учили TypeScript и какие преграды встречали на своем пути. Вы знаете еще какие-то ошибки, которые обычно совершают во время изучения TypeScript? Пишите в комментарии!

А еще...

Здесь говорю опять я, Павел. В конце еще раз приглашу вас в свой Telegram-канал. На канале Хороший разработчик знает я минимум три раза в неделю простым языком рассказываю про свой опыт, хард скиллы и софт скиллы. Я 15+ лет в IT, мне есть чем поделиться. Все это нужно разработчику, чтобы делать свою работу хорошо, работать в удовольствие, быть востребованным на рынке и получать высокую компенсацию.

А для любителей картинок и историй есть 🌄 Instagram.

Спасибо 🤗

Теги:
Хабы:
Если эта публикация вас вдохновила и вы хотите поддержать автора — не стесняйтесь нажать на кнопку
Всего голосов 13: ↑8 и ↓5+3
Комментарии47

Публикации

Истории

Работа

Ближайшие события