React Junior
207 subscribers
37 photos
462 links
Изучение React с нуля
加入频道
TypeScript. Декораторы методов класса

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

function methodDecorator(
target: any,
propertyName: string,
descriptor: PropertyDescriptor
) {

}

- target - это прототип класса. Для статического метода тут будет находиться функция-конструктор класса.
- propertyName - это название метода.
- descriptor - это дескриптор свойства (метода)

Дескриптор свойства - это объект, описывающий "настройки" свойства. У него есть поля:

- configurable,
- enumerable,
- value
- writable
- get
- set

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

function readable(target: Object, propertyKey: string, descriptor: PropertyDescriptor) {
descriptor.writable = false;
}

Используется он так же, как и декоратор класса, только над методом:

class User {
constructor(public name: string) {}

@readable
print(): void {
console.log(this.name);
}
}

Теперь для экземпляра класса User не получится переопределить метод print:

const john = new User('John');
john.print = function() {
console.log('Method print has been changed');
}
john.print();
// Cannot assign to read only property 'print' of object 'User'

#typescript #tsдекораторы
👍2🔥1
TypeScript. Декораторы параметров метода класса

Задекорировать можно даже параметры метода, хотя это уже идея посложнее.

Декоратор параметра принимает три аргумента:

- target - прототип класса или конструктор (для статических методов)
- propertyKey - название метода
- parameterIndex - порядковый номер параметра

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

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

function logParameter(
target: any,
key: string,
index: number
) {
const metadataKey = `__log_${key}_parameters`;
const loggedParams = target[metadataKey];
if (Array.isArray(loggedParams)) {
loggedParams.push(index);
} else {
target[metadataKey] = [index];
}
}

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

Теперь напишем декоратор для самого метода. Он переопределит реализацию метода, через его дескриптор (поле value). При вызове метода мы сначала выведем те параметры, которые были декорированы, а затем уже вызовем сам оригинальный метод.

function logMethod(
target: any,
key: string,
descriptor: PropertyDescriptor
) {
const originalMethod = descriptor.value;
descriptor.value = function(...args: any[]) {
const metadataKey = `__log_${key}_parameters`;
const loggedParams = target[metadataKey];

if (Array.isArray(loggedParams)) {
loggedParams.forEach(function(index) {
const arg = args[index];
console.log(`Метод: ${key}, параметр: ${index}`, arg);
})
}

const result = originalMethod.apply(this, args);
return result;
}
return descriptor;
}

#typescript #tsдекораторы
👍4
TypeScript. Декораторы свойств

Функция-декоратор свойства принимает два параметра:

- target - прототип или конструктор (для статических свойств) класса
- propertyKey - имя свойства

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

function formatProperty(target: any, propertyKey: string) {
let _value = target[propertyKey];

const newGetter = function(): string {
return `Mr./Mrs. ${_value}`;
}

const newSetter = function(newValue: string): void {
_value = newValue;
}

delete target[propertyKey];

Object.defineProperty(target, propertyKey, {
get: newGetter,
set: newSetter
})
}

Применяется декоратор свойства точно так же, как уже рассмотренные декораторы классов и декораторы методов:

class User {
@formatProperty
name: string;

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

print(): void {
console.log(this.name);
}
}

let john = new User('John');
john.print(); // Mr./Mrs. John

#typescript #tsдекораторы
👍2
TypeScript. Декораторы методов доступа (геттеров и сеттеров)

И последнее, что мы можем задекорировать в TypeScript, - это методы доступа к свойствам класса (геттеры и сеттеры).

Принимает такой декоратор три параметра:

- target - прототип класса или функция-конструктор класса (для статических свойств).
- propertyName - название метода.
- descriptor - дескриптор свойства

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

Пример декоратора-валидатора:

function validator(
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const originalSet = descriptor.set;

descriptor.set = function(value: string) {
if (!value) {
throw new Error(`Invalid value for property ${propertyKey}`);
}

if (originalSet !== undefined) originalSet.call(this, value);
}
}

Добавляем к сеттеру:

class User {
private _secret: string = "secret";

constructor(public name: string){

}

public get secret(): string {
return this._secret;
}
@validator
public set secret(n: string) {
this._secret = n;
}
}

let john = new User('John');
john.secret = ""; // Invalid value for property secret

#typescript #tsдекораторы
👍1🔥1
TypeScript. Декораторы

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

- классов
- методов
- параметров методов
- свойств
- методов доступа к свойствам (геттеров/сеттеров)

В зависимости от типа декоратор получает разные параметры:

- конструктор класса или его прототип
- название свойства/метода
- дескриптор свойства метода

В основном декораторы изменяют свои аргументы, а не возвращают новое значение. Если все же возвращают новое (например, новый дескриптор свойства), то оно заменяет старое.

Мы все время указывали декоратор вот так:

@имя_функции_декоратора

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

function decoratorsFactory(param) {
return function decorator(constructor: Function) {
//...
}
}

@decoratorsFactory('custom_value')
class User {

}

#typescript #tsдекораторы
👍2🔥1
TypeScript. Утилиты (Readonly, Required, Partial)

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

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

Readonly

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

Required

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

Partial

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

Все эти утилиты работают с одним типом данных, не изменяют его и создают новый тип, который сопоставим с исходным (но имеет другие модификаторы свойств).

#typescript
👍1🔥1
TypeScript: объединения и пересечения

Объединения в TS - это аналог логического ИЛИ. Означает, что переменная должна быть одного из перечисленных типов.

Пересечения - это логическое И. Берет несколько типов и комбинирует их в один (типа Object.assign).

#typescript
👍1🔥1
TypeScript. Ограничения обобщенных типов

Еще немного об обобщенных типах, или дженериках.

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

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

1)
Например, в функции getLength1 мы используем дженерик T, который может быть абсолютно любым типом. Даже таким, в котором нет поля length. Компилятор сразу же подчеркивает это.

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

2)
Для этого добавим интерфейс Lengthwise, который предусматривает наличие поля length и расширим его нашим дженериком - функция getLength2.

Ошибка пропала. Теперь компилятор сможет проверять входящие параметры.

3)
Если требуется обозначить только свойство length, в TypeScript есть встроенный интерфейс ArrayLike.

#typescript #дженерики
👍2
Пример ограничения дженериков: обновление значений свойств одного объекта из другого.

Мы ожидаем на входе целевой объект target (свойства которого нужно обновить) и объект-источник данных source (из которого будут браться новые значения). Важно, чтобы у source были те же поля, что и target, ведь из них будут браться данные. По сути, тип source расширяет тип target. У него есть все поля target, а также могут быть свои собственные.

#typescript #дженерики
🔥2
TypeScript. Оператор keyof

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

type Point = {
x: number,
y: number
}

type PointKeys = keyof Point; // 'x' | 'y'

Можно даже использовать его с примитивными типами, в этом случае получим набор имен встроенных методов:

type NumberKeys = keyof number; // "toString" | "valueOf" | "toFixed" | "toExponential" | "toPrecision" | "toLocaleString"

Пример использования (ограничение обобщенных типов)

Смотрим на функцию getProperty. Она получает объект, имя свойства и возвращает значение этого свойства у объекта.

Функция работает с двумя типами: T (объект) и K (имя свойства).
При K - существующее у объекта свойство, то есть один из ключей объекта.

K extends keyof T

При вызове функции компилятор получает аргументы и выводит их типы.

getProperty(developer, 'pay'); 

- тип T - это Staff
- тип K - это keyof T, а точнее "name" | "salary"

Компилятор смотрит на второй аргумент. Его тип pay и он не может быть присвоен объединению ключей Staff.

#typescript #дженерики
👍2
Асинхронный рендеринг в React 18

Видео (рус.): https://www.youtube.com/watch?v=vRq9UtVhP_8&t=3500s
В видео два доклада, нужный начинается на 58 минуте.

Небольшой доклад (~ 20 мин) о новом (относительно) конкурентном режиме в React. Можно смотреть на увеличенной скорости.

Начинается с краткого вступления о том, как вообще был задуман этот режим, зачем, и как он постепенно внедрялся в библиотеку (React Fiber, хуки - это все этапы долгого пути).

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

Затем на живых примерах разбираются три фичи конкурентного режима: Suspense, useTransition, useDeferredValue. Все это как раз способы указать приоритет различных задач.

В целом доклад очень понятный, позволит составить общее представление о Concurrent React, если еще не.

#ссылки #concurrentmode
🔥2
Используйте коллбэк-рефы вместо useEffect

Статья (англ.): https://tkdodo.eu/blog/avoiding-use-effect-with-callback-refs

Статья рассматривает и критикует обращение к рефам в React внутри хука useEffect. Например, так мы часто реализуем установку фокуса на поле ввода.


useEffect(function() {
inputRef.current?.focus();
}, []);


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

Было бы здорово привязать вызов функции именно к отрисовке самого поля ввода - и для этого приходят на помощь функциональные рефы. На самом деле в проп ref можно передавать не только объект, созданный через React.useRef, но и обычную функцию. Она вызывается как раз в момент отрисовки элемента и получает сам элемент в качестве параметра.


ref={ function(node) {
node?.focus();
} }


При удалении элемента функция вызывается снова, на этот раз с параметром null.

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

В статье, кроме того, есть пример использования этого подхода для получения метрик элемента.

Старый пост про коллбэк-рефы: https://yangx.top/react_junior/122

#рефы #хуки #статьи
👍4
Визуальный гайд по рендерингу в React

Серия статей (англ.):

- Часть 1. It Always Re-renders
- Часть 2. Props
- Часть 3. useMemo
- Часть 4. useCallback
- Часть 5. Context
- Часть 6. DOM

Вспоминаем, как происходит рендер и перерендер в React.

1) При изменении состояния компонента, он ререндерится.
2) Если ререндерится родительский компонент, ререндерятся и все его потомки, если только они не созданы с помощью React.memo.
3) При изменении пропсов, компонент ререндерится (даже если он создан с помощью React.memo). Важно при этом помнить про ссылочные типы данных (объекты, массивы, функции), которые могут выглядеть одинаково, но не быть при этом равны. Например, если передать в проп дочернего компонента анонимную функцию, то при каждом ререндеринге родителя это будет уже другая функция, поэтому потомок тоже будет перерендериваться, даже если обернут в React.memo. Чтобы этого избежать, используем хуки useMemo и useCallback.
4) Если компонент подписан на контекст, то при изменении контекста он ререндерится.
5) На самом деле при изменении контекста (глобального состояния приложения), перерендеривается корневой элемент приложения, а значит и все его потомки. Чтобы избежать этого, можно оборачивать первого потомка AppContext.Provider в React.memo.
6) Если в провайдер контекста передается ссылочный тип данных, который меняется при каждом рендеринге корневого элемента, то все будет работать как с пропсами в пункте 3. Нужно такой контекст мемоизировать.

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

#статьи #оптимизация #контекст #подкапотом
👍4
TypeScript. Преобразование к типу

Преобразование к типу (Type assertion) - явное указание типа значения какого-либо выражения.

Есть два способа сделать это:
1) Тип указывается в угловых скобках перед выражением.
2) Тип указывается после оператора as после выражения.

Пример на картинке.

Если явно не указать тип явно, компилятор будет ругаться, так как результатом работы метода getElementById может быть не только HTML-элемент, но и null.

#typescript
👍4🔥1
TypeScript. ReadonlyArray

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

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

У таких массивов нет методов, которые изменяют массив.

#typescript
👍4
TypeScript. Заголовочные файлы

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

console.log(globalVar); // Cannot find name 'globalVar'


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

declare let globalVar: string;


Эта строчка сообщает компилятору, что в коде может встретиться переменная с именем globalVar и типом string. Теперь он сможет оперировать ей.

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

declare function sum(a: number, b: number): number;

declare const user: {name: string, age: number, print: ()=> void};

declare class Person{
name: string;
age: number;
constructor(name: string, age: number);
display(): void;
}


Эти объявления обычно выносятся в отдельные заголовочные (декларативные) файлы с расширением .d.ts.

Если мы компилируем отдельный файл (tsc app.ts), то компилятор не сможет самостоятельно найти заголовочные файлы, которые используются, поэтому их нужно подключить с помощью директивы reference:

#typescript
🔥1
TypeScript. Заголовочные файлы для популярных библиотек

Основная область применения заголовочных файлов - использование крупных готовых библиотек вроде jQuery или, например, Knockout (моя боль 😅).
То есть мы используем переменные и функции библиотеки, о которых TypeScript ничего не знает, например,$. Их нужно описать.
Звучит очень страшно, но к счастью, для большинства популярных инструментов эта работа уже сделана.

В репозитории DefinitelyTyped можно найти заголовочные файлы для огромного количества библиотек.
Их можно установить через npm:

npm install --save-dev @types/jquery


Чтобы компилятор знал, где искать эти заголовочные файлы, нужно в файл tsconfig.json добавить опцию compilerOptions.typeRoots:

{
compilerOptions: {
//...
typeRoots: [ 'node_modules/@types' ]
}
}


#typescript
👍1
Стратегии перехода на TypeScript

Статья (рус., перевод): https://webformyself.com/strategii-perexoda-na-typescript/

В статье перечислены три стратегии:
- JS +TS в одном проекте, постепенное переключение
- добавление типов через JSDoc с последующим переключением на TS
- снэпшот-тесты для больших проектов

Мне очень захотелось подключить TS в мой текущий рабочий проект (на Knockout).
Решено было использовать первую стратегию и идти сверху вниз (или снизу вверх, как посмотреть 🤔, в общем переводить на TS мелкие файлы, которые импортируются более крупными).

Для этого потребовалось внести изменения в сборку. Для TS-файлов добавился ts-loader для Webpack. Получается, что при импорте TS-файла он сначала обрабатывается лоадером, а затем дальше участвует в сборке как обычный JS в составе файла, который его импортировал.

#typescript #проверкатипов #подключение #ссылки
👍4
Что такое K, T и V в TypeScript Generics

Статья (англ.): https://medium.com/frontend-canteen/what-are-k-t-and-v-in-typescript-generics-9fabe1d0f0f3

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

В двух словах, это плейсхолдеры для типов, которые будут определены только при вызове функции.

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

Буковки могут быть любые, T просто используется чаще всего (T - Type).

Еще популярны:
- K - Key
- V - Value
- E - Element

#ссылки #typescript #дженерики
👍2🔥1
Mapped Types в TypeScript

Статья (англ.): https://javascript.plainenglish.io/using-typescript-mapped-types-like-a-pro-be10aef5511a

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

Например, если у нас есть тип:

type User = {
name: string;
password: string;
address: string;
phone: string;
};


то из него можно получить:

type UserPartial = {
name?: string;
password?: string;
address?: string;
phone?: string;
};


Это самый банальный пример, на самом деле там все намного интереснее.

К посту прилагается картинка, на которой изображено "внутреннее устройство" утилит Partial, Required, Readonly и Pick, которые в общем и являются Mapped Types. На первый взгляд выглядит страшно, но статья подробно объясняет, как это работает, и как самостоятельно создавать типы с похожей функциональностью.

#typescript #подкапотом #ссылки
👍2🔥1
Своими словами:
в этих типах мы используем обобщения (дженерики).

Например, утилита Partial принимает некий тип T, неизвестно, какой конкретно, и на его основе создает новый тип.

Она берет все ключи исходного типа (keyof T) и делает их своими ключами. Каждый конкретный ключ обозначается типом P. То есть P - это каждый ключ типа T, и у нового типа будут те же самые поля.

Но к каждому полю добавлен значок ? - то есть поле становится опциональным, необязательным.

А типы полей ровно те же самые, что и у исходного типа - T[P]. Это обычный синтаксис доступа к свойствам объекта, но тут он применяется к типу.

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

#typescript
👍2🔥1