Site logo

Developer Blog

Pavel Koltyshev

Типизации кода в TypeScript

Содержание

Рассмотрим примеры интересных приемов типизации кода в TypeScript.

Вывести типы ключей и значений из enum

Представим что у нас есть enum:

enum Drink {
  Bear = 'bear',
  Lemonade = 'lemonade',
  Water = 'water',
}

Вывести из него типы ключей и значений можно так:

type DrinkKey = keyof typeof Drink; // "Bear" | "Lemonade" | "Water"

type DrinkValue = `${Drink}`; // "bear" | "lemonade" | "water"

const drinkPrices: Record<DrinkValue, number> = {
  bear: 100,
  lemonade: 30,
  water: 200,
};

Типизировать единицы измерений

Иногда полезно типизировать единицы измерения в константных значениях:

const sizes: Record<string, `${number}px`> = {
  small: '470px',
  medium: '800px',
  large: '1200px',
};

Сделать свойства обязательными

Рассмотрим оператор -? для установки свойств как обязательных:

interface Props {
  visible?: boolean;
  disabled?: boolean;
  readOnly?: boolean;
}

type RequiredProps = { [k in keyof Props]-?: Props[k] };

// или более простой вариант
type RequiredProps = Required<Props>;

Использование оператора satisfies

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

interface Box {
  name: string;
  color: number | string;
}

const box: Box = { name: 'Gift', color: 'red' };

box.color.toUpperCase(); // Error: Property 'toUpperCase' does not exist on type 'string | number'.

Мы можем обойти эту ошибку используя оператор satisfies, который появился в TypeScript 4.9:

interface Box {
  name: string;
  color: number | string;
}

const box = { name: 'Gift', color: 'red' } satisfies Box;

box.color.toUpperCase();

Автоматическая инициализация свойств в классах

У нас есть код который мы хотим упростить:

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

const user = new User('Mike');
console.log(user.name); // Mike

Если вы используете публичные свойства, то код выше можно переписать в более простой вариант:

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

const user = new User('Mike');
console.log(user.name); // Mike

Использование ключевого слова infer

Ключевое слово infer позволяет извлекать значение из типа.

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

type TypeOf<T> = T extends (infer U)[] ? U : never;

type ArrayNumber = number[];

type Num = TypeOf<ArrayNumber>; // number

Или будет извлекать тип значения у объекта:

type ValueOf<T> = T extends { [k: string]: infer V } ? V : never;

const sizes = { device: '470', tablet: '980', desktop: '1024' } as const;

type DeviceValue = ValueOf<typeof sizes>; // "470" | "980" | "1024"

Указать что значение не является null

У нас есть код в котором функция возвращает значение в виде объекта:

const getData = (): null | object => ({});

const data: object = getData(); // Type 'object | null' is not assignable to type 'object'

Нам нужно указать что значение возвращаемое из функции не равно null:

const getData = (): null | object => ({});

const data: object = getData()!; // используем оператор !

Постарайтесь избегать использование: !, any, as. Это сделает ваш код более понятным и предсказуемым.

Создать новый тип из другого типа выбрав нужные свойства

enum DogBreed {} // Порода собаки

interface Dog {
  name: string;
  age: number;
  breed: DogBreed;
}

type Cat = Pick<Dog, 'name' | 'age'>; // {name: string; age: number }

Создать новый тип из другого типа исключив ненужные свойства

interface Car {
  name: string;
  price: number;
  mileage: number;
}

type Boat = Omit<Car, 'mileage'>; // {name: string; price: number; }

Исключить тип из другого типа

type Foo = string | number | Date;

type Bar = Exclude<Foo, Date>; // string | number

Более сложный пример:

type Car = { brand: 'Audi'; price: number } | { brand: 'BMW'; price: number };

type Audi = Exclude<Car, { brand: 'BMW' }>; // {brand: 'Audi', price: number}

Извлечь тип из другого типа

type Spam = 'a' | 'b' | 'c';

type A = Extract<Spam, 'a'>; // 'a'

Более сложный пример:

type MyElement = { type: 'node'; children: MyElement[] } | { type: 'fragment'; children: MyElement[] };

type MyFragment = Extract<MyElement, { type: 'fragment' }>; // { type: 'fragment', children: MyElement[] }