Главная » Статьи » TypeScript написанный неправильно

TypeScript написанный неправильно

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

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

Чрезмерное использование типа any

Это классика, особенно для новичков в этом языке, потому что они видят в нем способ сказать: «Я понятия не имею, что будет внутри этой переменной, но позвольте мне использовать ее, хорошо?».

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

Представьте себе следующий пример:

let something: any;
let condition = true; if(condition) { something = JSON.parse('{ "name": "Fernando Doglio", "age": 37 }');
} else { something = JSON.parse('{ "race": "dog", "number_of_legs": 4, "sex": "Male"}')
} console.log(something)

Мы могли бы иметь дело со строкой, поступающей из файла или введенной пользователем. На самом деле мы не знаем, какой объект нам потребуется, мы просто знаем, что иногда речь идет о человеке, а в другом случае о животном. Так что значение something будет разным в зависимости от логики condition. Это не правильно. Почему? Потому что теперь мы не можем проводить никаких проверок с помощью something. Если бы мы хотели сослаться на одно из его свойств, оно было бы типом по умолчанию any, так что на самом деле любой тип, зависящий от something, автоматически должен избегать проверки типа. Мы создали каскадный эффект, который сводит на нет основную функцию TypeScript.

Таким образом, вместо того, чтобы полагаться на надоедливый тип any, вы можете определить тип something, который будет примерно таким же как unknown, но не совсем.

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

type Human = { name: string; age: number;
} type Animal = { race: string; number_of_legs: number; sex: 'Male' | 'Female';
} function parseit(str: string): unknown { return JSON.parse(str);
} let something;
let condition = true; if(condition) { something = parseit('{ "name": "Fernando Doglio", "age": 37 }') as Human; console.log("This is a person, named: ", something.name)
} else { something = parseit('{ "race": "dog", "number_of_legs": 4, "sex": "Male"}') as Animal; console.log("This is a ", something.race)
}

Ключевые изменения в коде:

Я перенес синтаксический анализ в отдельную функцию, которая возвращает тип unknown.

Я определил 2 разных типа (Human и Animal). Они оба являются потенциальными типами something, но мы не знаем, какой из них будет использован, пока код не запустится.

Я объявил something без типа, это ключ.

Логика использования something зависит от его типа и должна быть внутри каждой логической ветви (т.е. внутри каждого блока оператора if).

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

Использование типа Function вместо указания сигнатуры

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

Другими словами, вы можете написать такой код:

function myMainFN(callback: Function) { let date = new Date(); callback(date, "random string", 131412); let i = callback("this other string"); console.log(i, date, callback(123125091))
}

Обратите внимание, как я вызываю callback тремя разными способами. Я даже объявляю переменную с возвратом одного из этих вызовов. Хотите угадать тип i? Правильно, будет any. Вместо этого найдите время, чтобы понять сигнатуру функций, которые вы хотите использовать. Обратный вызов, вероятно, предназначен для получения некоторых специфических параметров и возврата определенного типа. Так что давайте определим его!

type myCallback = (error: Error|null, results: string) => void; function myMainFN2(callback: myCallback) { let date = new Date(); //your logic here... if(error) { //valid uses of callback callback(new Error("Something went terribly wrong, abort, abort!"), ""); } else { callback(null, "This is fine"); } //callback(date, "random string", 131412); - invalid because `date` is the wrong type and it also has 1 extra parameter //let i = callback("this other string"); - invalid because it's missing the first parameter //console.log(i, date, callback(123125091)) - invalid because we're calling it with the wrong parameter
}

Первое, что я сделал, я объявил тип функции обратного вызова: myCallback. Я объявляю ее как функцию, которая принимает 2 параметра, первый из которых потенциально возможно равен null, поскольку я использую шаблон «Сначала ошибки» для этого обратного вызова. А второй — просто строка. Внезапно все мои предыдущие вызовы обратного вызова становятся недействительными, и TS может сказать мне это. Фантастика! Определяя сигнатуру функции обратного вызова, я могу написать код, который должным образом полагается на нее, и это безопасно, и мне также удалось задокументировать способ структурирования своей функции обратного вызова любому, кто использует ее. Беспроигрышный вариант!

Предположение типа объектных литералов

В TypeScript есть очень интересная функция, называемая «вывод типа», в которой вам разрешено пропустить объявление типа в некоторых ситуациях, и, учитывая тип данных, которые вы используете, TypeScript сделает вывод за вас. Например:

let myValue = 2;
let mySecondValue = "This is a string";
let result = 2 - mySecondValue;

Этот код будет работать точно так же в JavaScript и будет возвращать значение. Это не имело бы никакого смысла, потому что фактически операция не имеет смысла, поэтому в результате вы получите NaN. Но все же интерпретатор будет запускать его. Однако, как только вы перенесете этот код в TS, он сообщит вам об ошибке, потому что TS уже сделал вывод, что myValue имеет тип number а mySecondValue имеет тип string, и вы не можете вычесть string из number, это так просто!

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

type Human = { name: string; age: number;
} type Animal = { race: string; number_of_legs: number; sex: 'Male' | 'Female'; owner: Human;
} let me = { name: "Fernando Doglio", age: 37
} let myDog = { race: "Dog", number_of_legs: 4, sex: "Male",
}

Хорошо, таким образом мы определили 2 объектных литерала, которые следуют структуре обоих типов Human и Animal, но, конечно, то, что они похожи на эти типы, не означает, что TS будет предполагать, что вы желаете следовать их структуре. Это синтаксически верный код, и пока у нас нет ошибок. Перейдем к примеру:

function whosYourOwner(a: Animal): string|null { if(a.owner) { return a.owner.name } return null;
} whosYourOwner(myDog) //BOOM

И тут начинаются проблемы. Вызов whosYourOwner не будет работать, потому что теперь у вас есть 2 проблемы с вашими объектами. Можете догадаться, что это за проблемы? Мы приводим литерал myDog к объекту Animal, и мы видим следующее:

С одной стороны, нам не хватает атрибута owner. Ой! Не беспокойтесь, мы это быстро исправим.

Но даже если мы это сделаем, у нас также есть проблема со свойством sex, потому что string(который является предполагаемым типом для этого свойства) не будет совместимым с enum ‘Male’|’Female’.

Да, у нас проблемы. Однако решение очень простое: назначить тип литералу вашего объекта во время объявления.

let myDog: Animal = { race: "Dog", number_of_legs: 4, sex: "Male",
};

Произойдут 2 вещи:

TS сообщит вам, что не хватает свойства owner. Он не помечен как необязательный в объявлении типа, поэтому здесь он потребуется.

Значение Male будет допустимой опцией для enum.

Другими словами, сделаем вывод:

Для основных типов: хорошо

Для сложных типов: не годится

Числовые enum – плохо, строковые enum — хорошо

Это может избавить вас от огромной головной боли и многих часов отладки. Как вы объявляете свои перечисления в TS? Я имею в виду, что весь их смысл в том, чтобы забыть о своих значениях и напрямую использовать свойства enum, если хотите. Верно? Итак, давайте представим, что мы создаем перечисление для вашего статуса проживания (т.е. являетесь ли вы гражданин, резидент своей страны или работаете по визе):

enum ResidencyStatus { Resident, Citizen, VisaWorker,
} let myStatus: ResidencyStatus; myStatus = ResidencyStatus.Resident; console.log(myStatus)

Приведенный выше код будет работать. Что будет на выходе последней строки? Будет 0 потому, что TS достаточно любезен, чтобы автоматически присвоить нам значение, когда мы его не указываем. Это замечательно и показывает, насколько неактуальны фактические значения внутри перечислений. Что с этим не так? Следующий код полностью применим и для TS:

let myStatus: ResidencyStatus; myStatus = 124124; console.log(myStatus)

Видите проблему? Я определил переменную типа ResidencyStatus, которая по определению должна позволять мне ограничивать значения, которые я ей присваиваю, но я все равно могу присвоить любое числовое значение, и TS не заметит разницы. Можем ли мы это решить? ДА, мы можем, и это не так сложно, нам просто нужно присвоить строковые значения нашим элементам внутри перечисления, и TS станет намного более сдержанным в отношении типа значений, которые мы присваиваем нашей переменной:

enum ResidencyStatus { Resident="res", Citizen="cit", VisaWorker="visa",
} let myStatus: ResidencyStatus; myStatus = "123"; console.log(myStatus)

Теперь этот код не будет работать, потому что TS не понравится, если вы назначите свойству значение «123″. Задача решена!

Атрибуты private класса через ключевое слово private

Это застало меня врасплох, когда я работал с TS. Я никогда не был большим поклонником самостоятельного добавления классов в JavaScript, учитывая, что модель Prototypal Inheritance работала отлично, и, честно говоря, классы на данный момент являются просто синтаксическим сахаром вокруг нее. Однако, когда TS вошел в мою жизнь, я начал видеть, как разработчики TS усердно работают над реализацией более полной модели ООП, так что это звучало привлекательно, и я решил попробовать. Но потом я ударился об стену и был немного зол.

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

class Person { private name: string; private age: number; constructor(n: string, a: number) { this.name = n; this.age = a; } get Name():string { return this.name; } set Name(newName: string) { this.name = newName; } get Age():number { return this.age; } set Age(newAge: number) { this.age = newAge; } } let oMySelf = new Person("Fernando Doglio", 37);
oMySelf.Age = 38;

Все в порядке, правда? Мы объявили оба наших свойства приватными, поэтому мы можем полностью контролировать, как и когда они будут обновляться. Вы не можете сделать oMySelf.name=»someone else», TS вам не позволит. Конечно, пока вы не сделаете что-то вроде этого:

for ( let prop in oMySelf) { console.log(Object.getOwnPropertyDescriptor(oMySelf, prop))
}

Или что-то вроде этого (лично мне больше нравится):

console.log((oMySelf as any).name)

Поскольку ключевое слово private работает только во время компиляции, среда выполнения не сможет вас ограничить. Таким образом, ключевое слово in может перебирать фактический список приватных свойств, и через них вы можете получать значения Object.getOwnPropertyDescriptor и даже дополнительную информацию из этих якобы защищенных и безопасных переменных. И не заставляйте меня приступать к преобразованию объекта к типу any, с результатом этого можно делать все, что угодно!

Хорошо, но речь идет только о чтении приватных данных, что может быть проблемой, но это не так плохо, как их изменение. Верно? Это все еще защищено, не так ли? Нет, давайте посмотрим на Object.assign. И чтобы проверить это, давайте назначим нашему классу Person приватное свойство, у которого нет геттера или сеттера:

class Person { private name: string; private age: number; private secret: number; constructor(n: string, a: number, s: number) { this.name = n; this.age = a; this.secret = s; } ....

Давайте создадим 2 объекта одного класса со случайными значениями secret:

let oMySelf = new Person("Fernando Doglio", 37, Math.random()); let someoneElse = new Person("Other Person", 1000, Math.random());

Мы можем увидеть, что они разные:

console.log((someoneElse as any).secret)
console.log((oMySelf as any).secret)

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

Object.assign(oMySelf, someoneElse)
console.log((someoneElse as any).secret)
console.log((oMySelf as any).secret)

Значение secret теперь то же самое. На самом деле между обоими объектами все одинаково. Мы фактически перезаписали все приватные значения oMySelf из someoneElse.

Можем ли мы предотвратить это? Да мы можем! Нам просто нужно использовать обозначения приватных свойств JavaScript вместо TypeScript:

class Person { #name: string; #age: number; #secret: number; constructor(n: string, a: number, s: number) { this.#name = n; this.#age = a; this.#secret = s; } get Name():string { return this.#name; } set Name(newName: string) { this.#name = newName; } get Age():number { return this.#age; } set Age(newAge: number) { this.#age = newAge; } }

Выглядит ужасно, но это предотвратит все наши проблемы:

in не будет перебирать наши приватные свойства, поэтому мы не можем получить их имена.

Object.Assign будут игнорировать приватные свойства, поэтому они не будут перезаписаны.

Попытка привести наш объект к типу any ничего не изменит, поскольку TS будет жаловаться на то, что вы пытаетесь получить доступ к приватному свойству вне класса.

Все проблемы решены!

Заключение

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

Автор: Fernando Doglio

Источник: blog.openreplay.com

Редакция: Команда webformyself.

Читайте нас в Telegram, VK, Яндекс.Дзен