Typescript: лучшие практики
- четверг, 23 ноября 2023 г. в 00:00:14
Всем привет👋 В последние годы среди фронтенд разработчиков 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 - очень полезная вещь, помогает определить от чего наследуется тип по иерархии типов (any -> number -> ...) и сделать сравнение.
Благодаря комбинации extends и тернарным операторам можно создавать условные конструкции:
type IsNumber<T> = T extends number ? true : false
IsNumber<5> // true
IsNumber<'lol'> // false
Это ключевое слово даёт невероятную гибкость при использовании Typescript. Не стоит его бояться, относитесь к нему, как к новой переменной. Рассмотрим пример:
type GetString<T extends string> = T extends `${infer R}.` ? R : ''
Этот тип очень простой, он берёт передаваемую строку и создаёт тип (переменную) R из T, затем выдаёт его.
GetString<'Hi.'> // 'Hi'
Используйте readonly
по умолчанию, это позволит избежать случайного перезаписывания типов в вашем интерфейсе.
interface User {
readonly name: string;
readonly surname: string;
}
Допустим, у вас есть массив, который приходит с бекэнда [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 // ошибка
Появился относительно недавно (в версии 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
Советую их изучить и применять на практике, порой они довольно сильно облегчают жизнь. Подробнее о них можно прочитать тут.
На некоторых проектах в реальном мире можно встретить использование типа 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
в проекте. Добавляем статью в закладки и не забываем использовать :)