progway — программирование, IT
2.73K subscribers
25 photos
1 video
246 links
Чат: @prog_way_chat

Разборы вопросов и задач с собеседований, мысли, полезные материалы и просто вещи, что мне интересны из мира IT

Полезности и навигация в закрепе

По всем вопросам: @denisputnov
Download Telegram
Комплексные состояния

Есть такой код:

const [userData, setUserData] = React.useState<IData>({
identifier: "",
password: "",
});


Как его можно исправить:

const [identifier, setIdentifier] = useState<string>("");
const [password, setPassword] = useState<string>("");


Почему я считаю что так лучше:
1. Проще следить за иммутабельностью состояния, так как не нужно постоянно разворачивать prev объект
2. Легче контролировать зависимости и сайд эффекты, если на состояние завязывается, например, useEffect
3. Визуально в использовании считается легче
4. useState вместо React.useState, ИМХО приятнее
5. Нет необходимости создавать ещё какой-то тип

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

@prog_way_blog#typescript #web #review
Лучшие практики типизации

В рамках ведения проектов и код-ревью я частенько натыкаюсь на странности написания TypeScript кода. В этом посте я постарался собрать самые частые ошибки, на которые точно стоит обратить внимание.

1. Лишний контекст
// плохо
type Person = {
personName: string;
personAge: string;
}

// замечательно
type Person = {
name: string;
age: string;
}


2. enum везде. Даже там, где не нужно.

Сила enum заключается в том, что помимо перечисления внутри контекста, enum можно использовать в качестве типа. Если вы не планируете использовать перечисления как тип, а лишь маппите что-либо в рамках одной сущности, то используйте константные объекты:
const Status = {
Success: 'success',
Fail: 'fail'
} as const;


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

3. Злоупотребление any. Я не противник использования any в случаях, где это действительно необходимо, но многие, даже высокого грейда разработчики, не понимают зачем конкретно он нужен. Прежде, чем воткнуть any, пожалуйста, попробуйте использовать unknown. Это сделает ваш код гораздо безопаснее.

4. Использование Definite Assertion оператора. Это когда вы утверждаете оператором восклицательного знака !, что значение точно есть, хотя оно может быть и undefined:
// так делать не нужно, лучше добавить дополнительное условие
const foo = (arg?: string) => parseInt(arg!)


5. Злоупотребление оператором as. Ровно та же проблема, что с any. Самый частый пример, что мне доводилось видеть:
type Person = {
name: string
}

// плохо
const person = {
name: "Denis"
} as Person

// замечательно
const person: Person = {
name: "Denis"
}


6. Отдельным и крайне важным для меня пунктом выделю оформление enum. Согласно всем стайл-гайдам мира, enum — сущность, группа, в единственном числе, оформленная определенным образом.
// плохо
enum OPERATION_STATUS {}
enum STATUSES {}
enum STATUS {}
enum HTTPStatus {}

// замечательно
enum OperationStatus {}
enum Status {}
enum HttpStatus {}


Обратите внимание на то, что Http я написал не капсом. Аббревиатуры в названии любых переменных оформляются именно так, а не как HTTP. Также название enum-a пишется в PascalCase. Таким же образом оформляются и ключи enum-a:
// плохо
enum Status {
SUCCESS,
FAIL
}

// замечательно
enum Status {
Success,
Fail
}


Это самые частые ошибки, что мне приходилось комментировать. Все эти правила можно прочитать в официальном TypeScript Handbook и Google JavaScript Style Guide. Это не мои выдумки — это правила, закреплённые индустрией.

Спасибо за прочтение, это важно для меня ❤️

@prog_way_blogчат#web #theory #typescript
Как сохранять состояние в URL

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

Представим, что у нас есть строковый инпут для имени пользователя, его состояние в URL может выглядеть следующим образом:

www.site.com/?name=progway


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

const [name, setName] = useQueryState("name");
const [surname, setSurname] = useQueryState("surname", "Putnov");


Первым аргументом будем указывать ключ в URL, а вторым — изначальное состояние. Далее рассмотрим внутреннюю имплементацию хука и начнём с его интерфейса, тут всё достаточно просто:

type QueryValue = string | undefined;

function useQueryState<I extends QueryValue>(
queryKey: string,
defaultValue?: I,
) {
// тело хука
}


Реализуем наш собственный хук на базе useSearchParams из react-router-dom для удобного доступа к состоянию URL-a. Если вы используете другой роутинг, можно использовать аналогичный механизм, который подойдёт именно в вашем проекте. Мы же будем его использовать для изменения URL-a и восстановления состояния из URL-a при маунте компонента. Если в URL-е уже есть состояние, мы будем использовать его. Если нет, будем использовать defaultValue:

const [searchParams, setSearchParams] = useSearchParams();

const [state, setState] = useState<QueryValue>(() => {
const targetValue = searchParams.get(queryKey);

if (targetValue !== null) {
return targetValue;
}

return defaultValue;
});


Ну а всё что нужно далее — реагировать на изменение состояния и обновлять URL в соответствии с ним. Если в инпуте вдруг окажется пустая строка, то мы просто удалим наше состояние из Query параметров

useLayoutEffect(() => {
setSearchParams((prev) => {
if (state) {
prev.set(queryKey, state);
} else {
prev.delete(queryKey);
}
return prev;
});
}, [state, queryKey, setSearchParams]);


Да и всё. Всё работает.

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

Спасибо за прочтение, это важно для меня ❤️

@prog_way_blogчат#theory #code #typescript #web #react
Дженерики

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

Generic Type — обобщённый тип — это тип, который позволяет создавать по своему подобию другие типы или же динамически вычислять типы в зависимости от входных данных. Тут я максимально отойду от какой-то академичности и приведу простую аналогию:

Тип — это переменная
Дженерик тип — это функция, которая возвращает тип

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

const name = "Denis"

const getPerson = (name) => ({
name,
type: "person"
})

const person = getPerson(name)


В этом примере у нас есть переменная, на основе которой мы можем получить некоторый объект person используя функцию getPerson. А теперь рассмотрим нечто похожее в типах:

type Status = "success" | "error"

type Response<T> = {
status: T,
message: string,
code: number,
}

type ApiResponse = Response<Status>


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

В таком контексте дженерики можно воспринимать как функции-конструкторы, которые, основываясь на некоторых входных параметрах, могут создавать новые типы. И из всего этого можно сделать вывод, что дженерики — это конструкторы типов.

На этом моменте у многих новичков часто возникает закономерный вопрос — а зачем их использовать? И ответ на него очень прост — почти за тем же, что и обычные функции.

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

// объявим сущности приложения
type User = {
name: string,
age: number,
}

type Task = {
completed: boolean,
title: string,
}

// ответ api в случае ошибки
type ResponseError = {
code: number;
message: string;
}

// ответ api в случае успеха
type ListResponseSuccess<T> = {
data: T[],
total: number;
}

// общий тип со всеми возможными
// состояниями ответа
type ListResponse<T> = ListResponseSuccess<T> | ResponseError

// типы для наших сущностей
type UserListResponse = ListResponse<User>
type TaskListReponse = ListResponse<Task>

// и их может быть сколько угодно...
type CarListResponse = ListResponse<Car>
type RoleListResponse = ListResponse<Role>
type UserGroupListResponse = ListResponse<UserGroup>
// и тд


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

Это самый базовый пример использования дженериков, который может встретиться на практике. Не забывайте, что дженерики есть не только у типов, а и у интерфейсов, классов, функций… Даже у React-компонентов и хуков (что в целом тоже либо класс либо функция).

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

Спасибо за прочтение, это важно для меня ❤️

#web #theory #typescript
Шпаргалка по Utility-типам в TypeScript

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

type Person = {
name: string;
surname: string;
age: number
}


Partial<T>
Делает все ключи необязательными
type PartialPerson = Partial<Person>

// то же самое, что
type PartialPerson = {
name?: string;
surname?: string;
age?: number
}


Required<T>
Обратное действие Partial — делает все ключи обязательными
Required<Partial<Person>> === Person


Readonly<Type>
Запрещает изменять поля объекта
const person: Readonly<Person> = {
name: "Denis",
surname: "Putnov",
age: 22
}

// ошибка, так как
// person нельзя изменять,
// только считывать (readonly)
person.name = 'Денис'


Record<Keys, Values>
Создаёт тип из ключей и значений, указанных отдельно
// ключи могут быть только строкой
// значения - только числом
type WordCounter = Record<string, number>

type Status = 'success' | 'error'
type StatusCounter = Record<Status, number>

// то же самое, что и Record ранее
type StatusCounter = {
success: number
error: number
}


Pick<Type, Keys>
Выбирает только нужные ключи из типа и возвращает новый тип
// из типа Person выбираем только свойства
// name и surname
type PersonName = Pick<Person, 'name' | 'surname'>

// то же самое
type PersonName = {
name: string
surname: string
}


Omit<Type, Keys>
Удаляет ненужные свойства из типа и возвращает новый тип
// Из типа Person исключаем свойство 'age'
type PersonName = Omit<Person, 'age'>;

// то же самое
type PersonName = {
name: string
surname: string
}


Parameters<Function>
Возвращает тип параметров функции
type Foo = (a: number, b: number) => string
type Params = Parameters<Foo>

Params // [a: number, b: number]


ReturnType<Function>
Возвращает тип возвращаемого параметра из функции
type Foo = (a: number, b: number) => string
type Return = ReturnType<Foo>

Return // string


Спасибо за прочтение, это важно для меня ❤️

@prog_way_blogчат#theory #typescript #useful
Как типизировать функцию мемоизации

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

Шпаргалка по используемым в посте utility-типам:

Parameters<T> - возвращает тип аргументов функции T
ReturnType<T> - возвращает тип возвращаемых значений функции T

const foo = (a: number , b: number) => 5

type Args = Parameters<typeof foo>
// [a: number, b: number]

type Return = ReturnType<typeof foo>
// number


Итак, наша функция принимает в себя другую функцию. Сразу же типизируем это:

function memoize(func: (...args: any[]) => any) {
// ...
}


Далее было бы неплохо типизировать возвращаемую функцию. Для этого мы можем использовать следующую конструкцию: (...args: Parameters<оригинальная функция>) => ReturnType<оригинальная функция>. И вот мы сталкиваемся с проблемой, что для корректной типизации возвращаемой функции нам необходимы типы Parameters и ReturnType, которые обязательно принимают дженерик. В качестве дженерика выступает тип оригинальной функции. Для удобства вынесем тип оригинальной функции в дженерик функции memoize:

function memoize<T extends (...args: any[]) => any>(func: T) {
// ...
}


И итоговый интерфейс функции у нас сильно растянется, но будет таким:

type AnyFunction = (...args: any[]) => any;
type MemoizedFunction<T extends AnyFunction> =
(...args: Parameters<T>) => ReturnType<T>

function memoize<T extends AnyFunction>(func: T): MemoizedFunction {
// ...
}


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

Далее типизация кэша, тут всё просто:

const cache = new Map<string, ReturnType<T>>();


По типизации, ключ — строка, а значение — тот тип, который возвращает оригинальная функция.

Далее рассмотрим итоговый код:

type AnyFunction = (...args: any[]) => any;
type MemoizedFunction<T extends AnyFunction> =
(...args: Parameters<T>) => ReturnType<T>

function memoize<T extends AnyFunction>(func: T): MemoizedFunction {
const cache = new Map<string, ReturnType<T>>();

return function(...args: Parameters<T>): ReturnType<T> {
const key = JSON.stringify(args);

if (!cache.has(key)) {
cache.set(key, func(...args));
}

return cache.get(key)!;
};
}


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

Спасибо за прочтение, это важно для меня ❤️

@prog_way_blogчат#code #typescript
Что такое структурная типизация

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

У нас есть два типа Fireman и Programmer, а также функция, принимающая объект типа Fireman:

type Fireman = {
name: string
}

type Programmer = {
name: string
}

const fireman: Fireman = {
name: "Alex"
}

const programmer: Programmer = {
name: "Denis"
}

const foo = (person: Fireman) => { ... }


Номинальная типизация:

foo(fireman) // OK
foo(programmer) // Error


Но почему во втором случае ошибка? Потому что разные типы: функция ожидает на вход Fireman, а получает Programmer. Сравнение происходит только по самому типу.

А вот пример со структурной типизацией:

foo(fireman) // OK
foo(programmer) // OK


В этом случае всё отработает корректно, но только потому что Programmer и Fireman абсолютно идентичны по своей структуре.

Также будет работать и в случае, если один из типов будет являться подмножеством другого:

type Fireman = {
name: string;
}

type Programmer = {
name: string;
age: number
}

const fireman: Fireman = ...
const programmer: Programmer = ...

const foo = (person: Fireman) => { ... }

foo(fireman) // OK
foo(programmer) // OK


Даже тут ошибки нет, а всё потому что тип Fireman полностью включен в тип Programmer и при вызове функции объект типа Programmer полностью удовлетворяет типу ожидаемой структуры, а следовательно и ошибки не будет. Для языка не важно, что у Programmer есть какие-то дополнительные поля. Главное, чтобы присутствовали все из типа Fireman и имели такой же тип.

В этом и заключается весь смысл структурной типизации.

Спасибо за прочтение, это важно для меня ❤️

#web #theory #typescript
В чем заключается разница между интерфейсом и типом?

Для меня большое удивление, что я до сих пор не смог найти нормального материала, где был бы описан этот вопрос. В этом после я планирую исправить эту несправедливость.

Тип — обозначается ключевым словом type — представляет собой либо описание структуры (объекта, функции…), либо набор других типов или структур:

// набор типов
type Identifier = string | boolean;

// описание структуры
type Person = {
name: string;
age: number;
}

// набор типов или структур
type Foo = Person | string;


Интерфейс — обозначается ключевым словом inderface — может описывать только структуры:

interface Props {
title: string;
visible?: boolean;
}


Особенности типов:
1. Только типами можно создать типы-объединения (они же union type):

type Identifier = string | number
type Animal = Dog | Cat


2. Только типами можно создавать кортежи, фиксируя тип элемента массива по индексу и длину массива:

type Cortage = [string, number, boolean]


3. Только с помощью типов можно создавать псевдонимы (они же alias):

type Name = string
type Author = Name


4. Типами проще и красивее создавать описание функций:

type Foo = (a: string) => number

interface Foo {
(a: string): number
}


Особенности интерфейсов:
1. Только интерфейсы имеют перегрузку типов:

interface Person {
name: string
}

interface Person {
age: number
}

const kate: Person = {
name: "Екатерина",
age: 24
}


2. Только интерфейсы могут быть имплементированы:

interface Person {}

class Fireman implements Person {}


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

Спасибо за прочтение, это важно для меня ❤️

#web #theory #typescript
Сужение типов и уточняющие тайпгарды

Часто в TypeScript коде можно столкнуться с тем, что типы недостаточно точны. Это бывает в случаях, когда мы определяем типы, например, через unionstring | number и других подобных случаях. Рассмотрим пример:

const value: string | number = ...

parseInt(value) // ошибка


В таком случае мы получим TypeError , а всё потому что переменная value имеет более общий тип, чем тот, что ожидает parseInt — эта функция ожидает первым аргументом на вход только строку. В таком случае нам может помочь сужение типов, которое нам дают дополнительные проверки — тайпгарды.

Тайпгарды — Type Guards — это языковые конструкции, проверки, которые позволяют определить или уточнить тип переменной средствами JavaScript. Тайпгарды бывают двух видов — уточняющие и определяющие.

Уточняющие тайпгарды — проверки, которые позволяют вывести более узкий тип из общего типа, например вывести string из типа string | number | boolean | null.

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

Есть несколько способов сузить тип:

1. Через оператор typeof

const value: string | number = ...

if (typeof value === 'string') {
// ошибки не будет, т.к.
// тип value в этом условии
// строго равняется "string"
parseInt(value)
}


2. Через ключевое слово in или метод hasOwnProperty

const value: any[] | number = ...

if ("map" in value) {
// value === array
value.map(...)
}

const animal = Cat | Dog = ...

if (animal.hasOwnProperty('meow')) {
// animal === Cat
animal.meow()
}


3. Через ключевое слово instanceof

const person: Fireman | Programmer = ...

// при условии что Programmer - класс
// а person - инстанс класса
if (person instanceof Programmer) {
person.code()
}


4. Через встроенные в язык функции проверки типа

const value: any[] | number = ...

if (Array.isArray(value)) {
value.map(...)
}


5. Через existed check, например, для null и undefined

const user: User | null = ...

if (user) {
console.log(user.name)
}


Также подобные проверки можно выносить в отдельные функции, которые обычно именуются по следующему шаблону — is + <название типа или интерфейса> , например, для типа User будет логично создать тайпгард isUser, вот небольшой пример:

type Account = User | Admin | Manager

const isUser = (account: Account): account is User => {
return account.type === "user"
}


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

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

Если более просто, то мы знаем, что если account.type === “user” , то перед нами ни что иное как User. В этом случае тип аккаунта является предикатом, на основе которого можно делать выводы о типе аккаунта без дополнительных проверок.

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

Спасибо за прочтение, это важно для меня ❤️

@prog_way_blogчат#theory #code #typescript
Определяющие тайпгарды

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

Определяющие тайпгарды — это функции, которые позволяют уточнить тип unknown переменной. Сразу рассмотрим пример и рассмотрим следующий интерфейс:

interface User {
name: string;
age: number;
roles: string[];
}


И представим, что в коде у нас есть некоторая переменная с неизвестным типом, которую мы хотим обработать:

const foo: any = ...

if (isUser(foo)) {
// обработать как пользователя
} else {
// обработать как что-то иное
}


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

function isUser(value: unknown): value is User {
const user = value as User;

return user !== null
&& typeof user === 'object'
&& typeof user.name === 'string'
&& typeof user.age === 'number'
&& Array.isArray(user.roles)
&& user.roles?.every(role => typeof role === 'string');
}


Это может выглядеть очень некрасиво, но главное, что работает)

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

Спасибо за прочтение, это важно для меня ❤️

@prog_way_blogчат#theory #code #typescript