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

https://monkeyuser.com

Что такое Util Types?

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

Опциональные поля в типах - Partial

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

interface UserData {
  uuid: string;
  username: string;
  age: number;
}

// Реализация такого типа ручками:
interface OptionalUserData {
  uuid?: string;
  username?: string;
  age?: number;
}

Трансформацию как в OptionalUserData можно сделать с помощью специального типа Partial<T>. Все Util Types содержат в себе дженерик, в который мы передаем тип, который нам нужно преобразовать.

Partial<T> преобразует все поля в типе и делает их опциональными:

interface UserData {
  uuid: string;
  username: string;
  age: number;
}

type OptionalUserData = Optional<UserData>; // Эквивалентно интерфейсу OptionalUserData, что был в листинге до этого

Преобразование всех полей в обязательные - Required

Required является противоположностью Partial - он делает все поля обязательными.

interface UserData {
  uuid: string;
  username: string;
  age?: number; // Опциональное поле
}

type RequiredUserData = Required<UserData>;

// Property 'age' is missing in type '{ age: number; }' but required in type 'RequiredUserData'.
const person: RequiredUserData = {
  uuid: "fff",
  username: "tokiory"
};

Создание строготипизированных объектов - Record

Record используется для того чтобы создавать строготипизированные объекты.

  • Тип T нужен для того чтобы указать тип ключа для объекта;

  • Тип U указывает на тип значения поля.

interface PetInfo {
  age: number;
  breed: string;
}
 
type CatName = "lucas" | "oiko" | "jean";
 
const cats: Record<CatName, CatInfo> = {
  lucas: { age: 1, breed: "Maine Coon" },
  oiko: { age: 15, breed: "Persian" },
  jean: { age: 16, breed: "British Shorthair" },
};

Важно

Если указать вместо CatName в примере выше string, то у вас пропадет Intellisense для данного объекта в редакторе. Дело в том что Typescript будет думать, что внутрь объекта с типом Record<string, ...> можно положить любую строку в виде ключа и не будет анализировать те ключи, которые вы предоставили внутри объекта.

Делаем типизированные значения с автодополнением

Если мы просто создадим Record<string, ...>, то автодополнения нам не видать.

Typescript как уже было сказано выше будет считать, что Record<string, ...> может иметь любое поле, что попадает под тип string:

interface PersonInfo {
  isOnline: boolean;
  age: number;
}

type PersonalInfo = Record<string, PersonInfo>;

const persons: PersonalInfo = {
  john12: {
    isOnline: true,
    age: 20
  },
  tokiory: {
    isOnline: false,
    age: 20
  },
};

// Если мы напишем "persons." - то Intellisense не поймет чего мы от него хотим

Вместо этого подхода мы должны использовать оператор satisfies, который ввели в Typescript 4.9.

interface PersonInfo {
  isOnline: boolean;
  age: number;
}

type PersonalInfo = Record<string, PersonInfo>;

const persons = {
  john12: {
    isOnline: true,
    age: 20
  },
  tokiory: {
    isOnline: false,
    age: 20
  },
} satisfies PersonalInfo;

// Если мы напишем "persons." - то Intellisense заработает!!!

Идея состоит в том, что оператор satisfies будет проверять каждое свойство и значение из persons на совместимость с исходными типами в PersonalInfo.

Например, поле john12 является частным случаем типа string, satisfies видит это
и понимает что ключ объекта удовлетворяет тому, что все ключи должны быть типом string и не выводит никакой ошибки.

Важно отметить, что в отличии от первого подхода, второй - только проверяет совместимость с исходными типами, сами
типы объекту persons не назначены, то есть persons имеет тип:

const persons: {john12: {isOnline: boolean, age: number}, tokiory: {isOnline: boolean, age: number}}

Типы для функций - Parameters и ReturnType

Данные типы используются для того чтобы извлечь типы из функций.

  • Parameters<T> - возвращает массив типов параметров в функции;

  • ReturnType<T> - возвращает возвращаемый тип функции.

function capitalize({line}: {line: string}): string {
  return s.length ? s[0] + s.slice(1) : s;
}

type CapitalizeFuncArgs = Parameters<typeof capitalize>; // [ { line: string } ]
type CapitalizeFuncReturn = ReturnType<typeof capitalize>; // string

Работа с полями интерфейса - Pick и Omit

Pick и Omit в основном используются с интерфейсами для удобной работы с полями.

  • Pick<T, U> - позволяет вернуть одно или несколько полей из интерфейса;

  • Omit<T, U> - позволяет вернуть весь интерфейс без определенных полей.

interface Person {
  name: string;
  surname: string;
  age: number;
  login: string;
  email: string;
}

type LoginInfo = Pick<Person, "login" | "email" | "name">; // => { login: string; email: string; name: string; }

type ShowName = Omit<Person, "login" | "email">;           // => { name: string; surname: string; age: number; }

Общие элементы и уникальные элементы - Extract и Exclude

  • Extract - возвращает все типы, которые есть и в одном, и в другом переданом союзе типов;

  • Exclude - возвращает все типы, которые есть или в первом, или в другом переданом союзе типов;

type MaleName = "Carl" | "Christian" | "Daren";
type FemaleName = "July" | "Daren" | "Ann";

type OnlyMaleName = Exclude<MaleName, FemaleName>; // "Carl" | "Christian"
type OnlyGeneralName = Extract<MaleName, FemaleName>; // "Daren"

Взаимодействие с типами строк

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

  • Uppercase<StringType> - Тип который преобразует все символы в верхний регистр;

  • Lowercase<StringType>- Тип который преобразует все символы в нижний регистр;

  • Capitalize<StringType>- Тип который преобразует первый символ в верхний регистр;

  • Uncapitalize<StringType>- Тип который преобразует первый символ в нижний регистр.

type Cat = "cAt";
type UpperCat = Uppercase<Cat>; // "CAT"
type LowerCat = Lowercase<Cat>; // "cat"
type CapitalCat = Capitalize<Cat>; // "CAt"
type UncapitalCat = Uncapitalize<Cat>; // "cAt"

Если вы передаете данным союзный тип, то Typescript обработает тип для каждого элемента союза:

type Pet = "doG" | "CaT";
type UpperPet = Uppercase<Pet>; // "DOG" | "CAT"
type LowerPet = Lowercase<Pet>; // "dog" | "cat"
type CapitalPet = Capitalize<Pet>; // "DoG" | "CaT"
type UncapitalPet = Uncapitalize<Pet>; // "doG" | "caT"

Вместо заключения ? 

Если вам понравилась данная статья - то вы всегда можете перейти в мой блог, там больше схожей информации о веб-разработке.

Если у вас остались вопросы - не стесняйтесь задавать их в комментариях. Хорошего времяпрепровождения! ??‍♂