Всем привет???? В последние годы среди фронтенд разработчиков Typescript используется практически везде по умолчанию, начиная небольшими пет-проектами и заканчивая огромнейшими веб-приложениями. Однако, до сих пор на некоторых проектах можно встретить кучу any
и Function
. Давайте разберемся используете ли вы этот невероятно мощный инструмент правильно?
Дженерики - всему голова
Используйте их везде, где только возможно. Это поможет лучше определять используемый тип в коде. К сожалению, их не часто используют, а зря.
function returnType<T>(arg: T): T {
return arg;
}
returnType<string>('Habr') // всё ок
returnType<number>('Habr') // ошибка
// ^ Argument of type 'string' is not assignable to parameter of type 'number'.
Если используете какой-то определённый тип, то обязательно используйте extends
:
type AddDot<T extends string> = `${T}.` // получает только строки, иначе ошибка
Тернарные операторы и extends
extends - очень полезная вещь, помогает определить от чего наследуется тип по иерархии типов (any -> number -> ...) и сделать сравнение.
Благодаря комбинации extends и тернарным операторам можно создавать условные конструкции:
type IsNumber<T> = T extends number ? true : false
IsNumber<5> // true
IsNumber<'lol'> // false
infer
Это ключевое слово даёт невероятную гибкость при использовании Typescript. Не стоит его бояться, относитесь к нему, как к новой переменной. Рассмотрим пример:
type GetString<T extends string> = T extends `${infer R}.` ? R : ''
Этот тип очень простой, он берёт передаваемую строку и создаёт тип (переменную) R из T, затем выдаёт его.
GetString<'Hi.'> // 'Hi'
readonly
Используйте readonly
по умолчанию, это позволит избежать случайного перезаписывания типов в вашем интерфейсе.
interface User {
readonly name: string;
readonly surname: string;
}
as const
Допустим, у вас есть массив, который приходит с бекэнда [1, 2, 3, 4]
и вам нужно использовать только эти четыре числа, то есть сделать массив иммутабельным. С этим запросто справится конструкцияas const
:
const arr = [1, 2, 3, 4] // сейчас тип number, можно передать любое число
arr[3] = 5 // [1, 2, 3, 5]
const arr = [1, 2, 3, 4] as const
// теперь тип представлен как readonly [1, 2, 3, 4]
arr[3] = 5 // ошибка
Satisfies
Появился относительно недавно (в версии 4.9), но уже завоевал мою любовь тем, что позволяет наложить ограничения, не изменяя тип. Это бывает очень полезно, когда вы используете разные типы, которые не имеют общих методов.
Например, у нас есть такой код:
type Numbers = readonly [1, 2, 3];
type Val = { value: Numbers | string };
// то есть, значением может быть, числа 1, 2, 3, либо строка
const myVal: Val = { value: 'a' };
Допустим, если у нас строка, то мы должны привести её к заглавным (большим) буквам. Если использовать такой код без Satisfies
, будет ошибка:
myVal.value.toUpperCase()
// ^ Property 'toUpperCase' does not exist on type 'Numbers'.
Но, если использовать Satisfies
, то всё будет в порядке:
const myVal = { value: 'a' } satisfies {value: string};
myVal.value.toUpperCase() // A
Utility types
Советую их изучить и применять на практике, порой они довольно сильно облегчают жизнь. Подробнее о них можно прочитать тут.
Вместо Function
На некоторых проектах в реальном мире можно встретить использование типа Function
, однако эта практика является нежелательной. Вместо этого лучше передавать заранее описанный тип, либо же использовать подобную запись:
F extends (...args: unknown[]) => unknown
// где unknown заменить на те типы, которые вы используете в функции
К слову, тут же может пригодиться и infer
для получения аргументов и возвращаемого значения:
const func = (a: number, b: number): string => 'Hello'
// для примера, функция с аргументами a и b, которая выводит Hello
type GetArgs<F> = F extends (...args: infer A) => unknown ? A : never
GetArgs<typeof func> // аргументы - [a: number, b: number]
type getReturnType<F> = F extends (...args: never[]) => infer R ? R : never
GetReturnType<typeof func> // string
Лучше юнионы
Иногда можно встретить подобный код:
interface User {
loginData: "login" | "username";
getLogin(): void;
getUsername(): void;
}
Этот код плох тем, что можно использовать username, но при этом обращаться к getLogin() и наоборот. Чтобы этого не допустить, лучше использовать юнионы:
interface UserWithLogin {
loginData: "login";
getLogin(): void;
}
interface UserWithUsername {
loginData: "username";
getUsername(): void;
}
type User = UserWithLogin | UserWithUsername;
Юнионы итерабельны, то есть их можно использовать для прохождения циклической проверки:
type Numbers = 1 | 2 | 3
type OnlyRuKeys = {[R in Numbers]: boolean}
// {1: boolean, 2: boolean, 3: boolean}
Вместо заключения
Надеюсь, теперь вы будете лучше разбираться с Typescript и избавитесь от бесконечных any
в проекте. Добавляем статью в закладки и не забываем использовать :)
Комментарии (8)
iliazeus
22.11.2023 06:55К слову, тут же может пригодиться и infer для получения аргументов и возвращаемого значения:
Тут лучше подойдут стандартные
Parameters
иReturnType
.danila_egorenko Автор
22.11.2023 06:55Полностью согласен, это для примера, что с infer и функциями можно создавать довольно сложные типы
iliazeus
22.11.2023 06:55+3as const
Допустим, у вас есть массив, который приходит с бекенда
Пример неудачный -
as const
можно использовать только для compile-time констант, для данных с бекенда он ничем не поможет.
insecto
22.11.2023 06:55+2Используйте их везде, где только возможно.
И превратите свой код в невыносимую перловку из угловых скобочек вопросиков и неверов.
Alexandroppolus
22.11.2023 06:55+2Да, правильнее было бы сказать - используйте только при необходимости. Пример:
// ненужный генерик function getLen<T extends {length: number}>(x: T): number { return x.length; } // нормальный вариант без излишств function getLen(x: {length: number}): number { return x.length; }
Советы из TFM по генерикам - тут
Zukomux
22.11.2023 06:55Используйте
readonly
по умолчанию, это позволит избежать случайного перезаписывания типов в вашем интерфейсеПоправочка, не в интерфейсе, а в объекте заданного типа. На этапе компиляции будет проведена проверка, что вы пытаетесь присвоить значение свойству, которое указано как readonly.
И стоит добавить сюда описание что такое type и что такое interface. В чем между ними основная разница и что предпочтительнее использовать. К сожалению, последнее мало кто понимает.
Alexandroppolus
Можно. Чтобы гарантировать ровно одно из двух полей, надо "запретить" второе:
Так же тут следует упомянуть discriminated unions
danila_egorenko Автор
Спасибо за замечание, изменил пример, чтобы не вводить в заблуждение