Особый аспект понимания TypeScript для начинающих.

Когда я впервые изучил TypeScript, я заметил, что код TS полон нотаций, которые трудно понять. Некоторые обозначения также появляются в JavaScript, но когда они появляются в TypeScript, они имеют совершенно другое значение. Раньше меня это очень сбивало с толку.
Позже, при дальнейшем изучении, я понял использование этих обозначений и почувствовал их очарование. Вот 10 обозначений, которые я тогда считал странными, но теперь считаю очень полезными.
Оглавление:
· Unions type: |
· Intersection Types: &
· Numeric Separators: _
· Non-null assertion operator: !
· Optional notation: ?
∘ Optional Chaining: ?.
∘ Optional Properties: ?:
· Nullish coalescing operator: ??
· Private property: #
· Angle brackets: <>
∘ Type assertion
∘ Generic types
· Decorators: @
1. Тип союзов: |
В JavaScript | - это обозначение битовой операции:
Оператор побитового ИЛИ (
|) возвращает1в каждой битовой позиции, для которой соответствующие биты одного или обоих операндов равны1s.
Например, такой код JavaScript при запуске напечатает 7 в консоли:
const a = 5; const b = 3; console.log(a | b); // expected output: 7

Как это работает?
Потому что двоичное представление числа 5 - это 101, а двоичное представление числа 3 - это 011. Вычислите соответствующий бит с помощью ИЛИ, и вы получите 111, что в десятичной дроби равно 7.

Хотя TypeScript является расширением JavaScript, вы также можете использовать этот оператор в коде TS.
Код TypeScript :
let a: number = 5; let b: number = 3 console.log(a | b) // 7
Однако в TypeScript мы также используем | для обозначения типов объединения.
Например, если переменная может быть строкой или числом, мы могли бы написать:

let val: string | number; val = 1; val = "hi";
Иногда тип имеет только несколько возможных значений, поэтому мы также можем использовать типы объединения для их представления. Например, существует всего несколько часто используемых HTTP-методов, поэтому мы можем определить их следующим образом:

type HttpMethod = "GET" | "POST" | "PATCH" | "DELETE";
function xhr(method: HttpMethod, url: string): void {
console.log(`${method}: ${url}`);
}
Затем мы можем увидеть, как TypeScript предоставляет нам проверку типов, когда мы используем указанные выше функции:

TS напомнит нам, что метод xhr не поддерживает Get в качестве аргумента, что помогает нам избежать многих возможных ошибок типа.
Давайте посмотрим на еще один странный код:

type BooleanType = 0 | 1; let flag: BooleanType; flag = (0 | 1) as BooleanType; console.log(flag);
Как мы понимаем этот код?
Прежде всего, мы определяем BooleanType. Возможное значение этого типа - 0 или 1, а символ | обозначает тип объединения. Затем мы объявляем переменную flag типа BooleanType.
Затем мы присваиваем значение 0 | 1 flag, где | - побитовый оператор. Чтобы обеспечить совместимость типов, мы используем оператор as, чтобы заставить результат 0 | 1 быть логическим типом.
Итак, результат выполнения приведенного выше кода равен 1.
Конечно, в этом коде нет никакого смысла, но я написал его таким образом, чтобы показать вам два варианта использования |. Что ж, кажется, я делаю вещи еще более странными ...
2. Типы пересечений: &
Подобно | в JavaScript, & также является обозначением для битовой операции:
Поразрядный оператор И (
&) возвращает1в каждой битовой позиции, для которой соответствующие биты обоих операндов равны1s.
В TypeScript & также может использоваться для обозначения пересечения типов.
Например, есть два интерфейса:
interface Runnable{
run(): void;
}
interface Swimmable{
swim(): void;
}
Затем мы хотим определить новый тип, Гусь и Гусь могут как бегать, так и плавать, так как же нам это сделать?
Нам просто нужно объединить два типа вместе с &:
type Goose = Runnable & Swimmable;
Вот код TS:

Если параметр не удовлетворяет оба интерфейса, компилятор сообщит об ошибке.

3. Числовые разделители: _
Это обозначение используется для разделения чисел. На самом деле это синтаксис JavaScript, но я нашел его полезным, поэтому оставлю его здесь.
Когда мы пишем большое число, мы можем написать что-то вроде этого:
let total = 12123421;
На этом этапе, поскольку число такое длинное, человеку, читающему код, часто требуется несколько секунд, чтобы подсчитать длину числа. Вместо использования числового разделителя мы могли бы написать:

С _ мы можем быстро прочитать число: 12 миллионов 123 тысячи и 421. _ используется для разделения числа, не имеющего практического значения. Такие грамматические конфетки могут помочь нам написать большие числа, которые легче читать.
4. Ненулевой оператор утверждения:!
В JavaScript ! - это логический оператор, используемый для инвертирования логического значения.
Оператор логического НЕ (
!) (логическое дополнение, отрицание) переводит истину в ложь и наоборот.
Вот код JavaScript:
!false // return true !true // return false let flag = false; flag = !flag; console.log(flag); // true

В TypeScript вы по-прежнему можете использовать его как логический оператор. Однако у этой нотации есть дополнительное применение.
Возьмем такой код TypeScript:

В приведенном выше коде функция reverse принимает строку в качестве аргумента. Тип maybeString - это тип объединения строки и нуля. Если мы используем revere(maybeString) напрямую, компилятор сообщит об ошибке.
Но если мы внимательно посмотрим на код, мы знаем, что Math.random() всегда возвращает число от 0 до 1, поэтому, когда это тернарное выражение выполняется, maybeString должно быть строкой, а не нулем.
Мы можем быть на 100% уверены, что revere(maybeString) безопасно, а компилятор - нет. В этом случае мы можем использовать ненулевой оператор утверждения: !

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

В приведенной выше функции, если мы вызовем getRandomString() напрямую, компилятор предупредит нас, что параметр может быть неопределенным. Однако, если вы уверены, что параметр не будет неопределенным, вы можете использовать ненулевой оператор утверждения, чтобы подтвердить его.
Важно отметить, что подсказка компилятора разумна. Когда вы используете !, вам нужно убедиться, что операция безопасна.
5. Необязательное обозначение: ?
У этого символа есть два производных использования: ?. и ?:.
Необязательная цепочка:?.
Иногда мы не уверены, обладает ли объект определенным свойством, что может затруднить написание нашего кода.
Вот код JavaScript:
function getCity(user){
return user.location.city
}
Если у параметра нет поля местоположения, это вызовет ошибку во время выполнения:

Если мы хотим написать более надежный код на JavaScript, мы можем сделать только это:
function getCity(user){
return user.location ? user.location.city : undefined
}
Прежде чем пытаться получить location.city, проверьте, существует ли свойство user.location.

Но такой способ написания немного хлопотный, не очень удобный. TS дает нам синтаксический сахар: ?.
function getCity(user: any){
return user.location?.city
}
getCity({name: 'Jon'})
Если вы используете ts-node для выполнения вышеуказанного кода, все будет в порядке. Если user.location имеет значение null или undefined, он прекратит доступ к своему свойству city. Это гарантирует безопасность нашего кода.
Если мы скомпилируем приведенный выше код, используя tsc, результат будет выглядеть так:
function getCity(user) {
var _a;
return (_a = user.location) === null || _a === void 0 ? void 0 : _a.city;
}
В приведенном выше коде void 0 равно undefined. Вы не зря можете увидеть ответ на Stack Overflow. Если user.location равно нулю или неопределенному, он вернет undefined вместо получения свойства location.city.
Это обозначение также можно использовать для вызовов функций. Вот код JavaScript:
function run(obj){
obj.run()
}
Эта функция имеет тот же недостаток, что и предыдущая, то есть мы не уверены, есть ли у obj метод выполнения, а если нет, среда выполнения сообщит об ошибке.

И мы можем написать код TypeScript, например:
function run(obj: any){
obj.run?.()
}
run({})
Этот код такой же, как и раньше. Если obj.run не существует, не вызывайте метод.
Дополнительные свойства:?:
Помимо использования необязательных операторов при попытке получить свойство, мы также можем использовать их при определении свойства.
Например, следующий код вызовет ошибку во время компиляции:
interface User{
name: string;
email: string;
}
let user1: User = {
name: "Jon",
};

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

Мы добавляем к нему необязательное обозначение ?, чтобы сообщить компилятору, что это свойство является необязательным. После компиляции приведенного выше кода он не вернет ошибку.
6. Нулевой оператор объединения: ??
??: Если левый операнд равен нулю или не определен, он возвращает правый операнд, в противном случае - левый операнд.
let foo = null ?? "default string"; console.log(foo); // default string let bar = 0 ?? "default string"; console.log(bar); // 0 let value = "value" ?? "default string"; console.log(value); // value

7. Частная собственность: #
Частная собственность для класса - это очень распространенное требование, которое мы можем объявить с помощью нотации #.
class Person{
#name: string;
constructor(name: string) {
this.#name = name
}
greet(){
console.log(`Hello, I am ${this.#name}`)
}
}
Если поле начинается с #, вы не можете получить доступ к свойству вне метода класса:

Некоторые из вас также могут знать, что ключевое слово private также можно использовать для объявления частного поля, так в чем разница?
Ответ заключается в том, что частные свойства, объявленные ключевым словом private, действительно существуют в экземпляре, и мы можем принудительно получить к ним доступ. Частные свойства, объявленные #, не хранятся непосредственно в экземпляре.

Мы можем сначала преобразовать объект в тип any, а затем получить доступ к закрытым полям, определенным private, что невозможно для частных полей, определенных #.
8. Угловые скобки: ‹›
Эта нотация используется в двух местах: в одном для утверждений типа и в другом для универсальных типов.
Утверждение типа
Следующий код выдает ошибку при компиляции:

Поскольку компилятор считает someValue строкой или числом, он не соответствует типу параметра doubleString. Но когда мы анализируем код, мы знаем, что someValue должен быть строкой. Затем мы можем использовать утверждение типа, чтобы сообщить компилятору, что переменная должна быть строкой.

На этот раз компиляция приведенного выше кода не вернет ошибку.
Однако TSLint не рекомендует писать таким образом; вместо этого рекомендуется использовать as:

Эффект обоих синтаксисов одинаков: они утверждают тип переменной.
9. Универсальные типы
Использование TypeScript дает нам систему типов, которая помогает нам писать более безопасный код. Но это также приносит в жертву гибкость JavaScript.
Например, если бы мы написали функцию add, она приняла бы два аргумента. Если параметры являются строками, объедините их вместе; Если параметры числовые, то сложите их:
В JavaScript этот код довольно прост:
function add(a, b){
return a + b
}
Однако это сложно реализовать в TypeScript. Вы не можете написать это:
function add(a: string | number, b: string | number) {
return a + b;
}
Поскольку такие типы объединения не могут быть добавлены напрямую, компиляция приведенного выше кода вернет ошибку.
Или вы можете написать:
function addString(a: string, b: string) {
return a + b;
}
function addNumber(a: number, b: number) {
return a + b;
}
Можно писать так, но это хлопотно.
Как видите, в обеих вышеупомянутых функциях они имеют одно и то же тело. Единственная разница в том, что один обрабатывает строковые параметры, а другой числовые.
Мы также можем рассматривать тип как параметр, который необходимо передать. В этом случае мы можем написать:
function add<T>(a: T, b: T){
return a as any + b
}
Приведенный выше код означает, что функция add еще не знает, какой тип данных ей нужно обрабатывать, поэтому мы можем передать этот тип в качестве параметра при его использовании.
Нравится:

Таким образом, наша функция add может обрабатывать как строки, так и числа, и даже BigInt.
Обобщения позволяют мне наслаждаться безопасностью системы типов без потери гибкости JavaScript.
10. Декораторы: @
Допустим, у нас есть такая функция:
function hello(name) {
console.log("Hello " + name)
}
Мы хотим распечатывать журнал каждый раз, когда мы вызываем hello, так что мы можем сделать?
В JavaScript нет возможности сделать это напрямую, но мы можем обернуть hello функцией декоратора:

Результаты, достижения:
Start execute function "hello" at 2021-03-17T04:15:47.724Z Hello Jack
Но в JavaScript это не изящно. TS предоставляет синтаксис декоратора, чтобы лучше реализовать это.
Например, вот наш класс Friend:
class Friend {
name: string;
constructor(name: string) {
this.name = name
}
hello() {
console.log(this.name + " says: Hello")
}
}
Мы хотим распечатать журнал при вызове метода hello, поэтому мы можем указать функцию-декоратор logging для украшения метода hello.
Если вы хотите определить декоратор, который украшает метод класса, функция декоратора должна быть в следующем формате:
function logging(target: any, propertyKey: string,
descriptor: PropertyDescriptor) {
}

Где target - объект экземпляра, который будет вызывать метод в будущем, propertyKey - имя свойства соответствующего метода, а descriptor - дескриптор свойства. Это фиксированный синтаксис для декораторов, которому вы должны следовать.
Затем мы можем записать конкретную логику в метод ведения журнала.

После компиляции и запуска приведенного выше кода консоль должна напечатать что-то вроде этого:
Start execute function "hello" at 2021-03-17T01:31:11.607Z Jon says: Hello
Декораторы добавляют новые функции к существующему методу, не изменяя сам метод. Представьте, что если бы нам нужно было добавить функцию ведения журнала для многих методов класса, мы могли бы просто добавить декоратор перед каждым методом класса, не изменяя сам метод.
Фабрики декораторов
Предыдущий декоратор журналирования ограничивался печатью только сообщения даты. Фактически, мы можем использовать фабрику декораторов, серийное производство декораторов различных характеристик.
Нравится:

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

Скомпилируйте и выполните приведенный выше код, и результаты будут следующими:
Start execute function "hello" at 2021-03-17T01:49:05.087Z message: executing Person#hello Jon says: Hello
Заключение
И вот оно! Я надеюсь, что это помогло прояснить некоторые функции TypeScript и некоторые сложности, которые с ним связаны. Обязательно поделитесь своими мыслями в комментариях. Спасибо!