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

Содержание

1. Как типизировать данные
2. Как понимать типы
3. Как использовать типизацию
4. Глубокая типизация
5. Связанная типизация
6. Изящная типизация

Какие имена давать типам?

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

type Achievement = {
  name: string;  
}

type TAchievement = {
  name: string; 
}

type AchievementType = {
  name: string;
}

interface IAchievement {
  name: string;
}

enum Tabs {
  MAIN = 'main',
  CONTACTS = 'contacts'
}


enum ETabs {
  MAIN = 'main',
  CONTACTS = 'contacts'
}

enum TabsEnum {
  MAIN = 'main',
  CONTACTS = 'contacts'
}

Код 3.1. Префиксы и постфиксы в именах типов

В целом префиксы не несут никакого особого смысла. Как показано в Коде 1.5 из статьи "Как типизировать данные" имена из контекста значений и контекста типов не конфликтуют. А описание того, что за сущность содержится в типе ложится на плечи основной части имени. Тем более префиксы не нужны, чтобы определить что перед нами переменная или тип - контексты значений и типов не пересекаются.

Конфликт имён типов и переменных возникает при импорте типа и React-компонента. Данная ситуация не редкость для React-приложений и легко решается:

import { Achievement as AchievementType } from './types';
import { Achievement } from './components';

Код 3.2. Импортирование типа и компонента с одинаковыми именами

Стоит задуматься о необходимости уникального имени типа в рамках проекта. Не всегда комфортно, введя в поиск имя компонента, видеть в результатах мешанину из типов и переменных. Но когда вы типизируете пропсы для компонента, вряд ли вам понадобится уникальное имя для типа, который используется только в одном месте.

Следуя принципу единообразия, нужно именовать типы, как это принято на проекте, но если у вас есть возможность выбирать, используйте здравый смысл.

Где описывать типы

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

Тип переменной конечно описывается рядом с ней, но если он имеет довольно ёмкое описание, то лучше вынести его в отдельный type на уровень модуля. Соответственно, если в модуле для логики слишком много описанных типов, то их удобно так же вынести в отдельный файл, а разросшийся файл с типами разбить на несколько.

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

interface или type для объектных типов

type умеет почти всё, что умеет interface и немного больше. Полный отказ от interface не приводит к проблемам в большинстве проектов. Interface умеет редекларировать ранее описанный тип, что в редких случаях полезно для расширения типизации:

 interface Array<T> {
   findLast<S extends T>(predicate: (this: void, value: T, index: number, obj: T[]) => value is S, thisArg?: any): S | undefined;
   findLast(predicate: (value: T, index: number, obj: T[]) => unknown, thisArg?: any): T | undefined;
 }

Код 3.3. Повторная декларация interface Array добавляет в него новый метод findLast (тип описан по примеру метода find).

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

Отказаться от type не получится, так как он используется в объединении и пересечении типов, что является очень полезной возможностью. Но разработчик свободен в выборе способов типизации и конечно же стоит следовать принятым практикам на проекте.

Запрет на использование типа any

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

Отсутствие в коде any ещё не означает, что он не используется, так как этот тип неявно проставляется в параметры и другие переменные, чей тип анализатор не может определить по коду.

// param приобретает тип any

type Fn = (param) => void 

const myFn = (param) => {
  //...
}

Код 3.4. Параметры неявно принимают тип any

В этой ситуации запрет any через линтеры немного наивен. Кроме того иногда наш код готов работать с любым типом. Тогда можно прописать тип unknown. Анализатор, увидев этот тип, будет следить за тем, чтобы при передаче такой переменной происходило сужение типа до требуемого (что можно сделать через условия или защитники типа).

const myFn = (param: unknown) => {
  if (typeof param === 'string') {
    
    // здесь с помощью typeof (защитник типа)
    // тип unknown сужен до типа string
    // и мы можем использовать переменную param как строку

    return;
  }

  if (param instanceof Date) {

    // с помощью защитника типа instanceof можно опредилить,
    // что param является объектом даты.

    return;
  }

  // и т.п.
}

Код 3.5. Примеры сужения типа unknown различными способами

Избегание оператора as

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

const langs = {
  prop1: 'TS',
  prop2: 'JS'
}
const getLang = (propName: string) => langs[propName] as string;

Код 3.6. Сужение типа string | undefined к типу string с помощью оператора as

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

Есть ситуации, когда анализатор не способен определить тип и тогда as является не только не запрещённым, но и правильным решением.

Авторское отступление

В литературе пишут, что прописывая as, разработчик как бы говорит "TypeScript, я гарантирую, что здесь такой тип". Но учитывая, насколько неправильно разработчики используют as, эта фраза скорее звучит так: "TypeScript, заткнись! Я лучше тебя знаю, какой здесь должен быть тип".

Эти ситуации имеют конкретные причины:

  • TypeScript не может определить тип в результате парсинга JSON, строк или ArrayBuffer, так как хранимые в них данные имеют более узкие типы, чем возвращают методы и нет возможности заранее это узнать;

  • типизация нативных или библиотечных функций такова, что не позволяет вывести из входных данных более узкие типы (например reduce). Разработчик не может повлиять на их типы и вынужден преобразовывать результат через as.

// getItem всегда возвращает тип string,
// но функция требует более узкий тип

type Level = 'senior' | 'middle' | 'junior'ж

const currentColor = localStorage.getItem('level') as Level;
const getPermissions = (level: Level) => {
  // ...
}

// при создании новой структуры метод reduce ориентируется на тип
// второго аргумента, но его значение может не соответствовать
// типу результата.

type PropContainer = {
  prop1: boolean;
  prop2: boolean;
}

const props = ['prop1', 'prop2'].reduce(
  (acc, prop) => ({
    ...acc,
    [prop]: true,
  }),
  {} as PropContainer
);

Код 3.7. Примеры, где as помогает сужать тип

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

Типизация функций

Функции могут быть типизированы различными способами:

//тип задаётся переменной, которой присваивается функция

type Fn = (num: number) => string
const fn1: Fn = (num) => `id-${num}`;


// тип задаётся аргументам и возвращаемому значению

const fn2 = (num: number): string => `id-${num}`;


// тип аргументов задается как массив или кортеж

const fn3 = (...arg: [number]): string => `id-${arg[0]}`;


// тип функции, которая принимает любые аргументы

type FnWithAnyArgs = (...arg: any[]) => void;


// функция принимает тип через дженерик и типизирует
// с его помощью аргументы и возвращаемое значение

const fn4 = <T>(param: T): [T] => [param];

Код 3.8. Типизация функций различными способами

Излишне использовать все эти способы одновременно. Кроме того, тип возвращаемого значения может быть вычислен анализатором TypeScript.

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

Использование существующих типов

Библиотеки предоставляют типы для работы со своими данными. Прежде, чем придумывать новый тип, проверьте в файлах декларации d.ts предоставленные типы, возможно похожий тип уже есть и логичнее использовать его.

// React-компонент использует кастомную типизацию

import React from 'react';

type Props = { children: React.ReactNode }

const MyComponent = (props) => (
  <>props.children</>
)

// React-компонент может быть типизирован существующими типами

import { FC, WithChildrenProp } from 'react';

const MyComponent: FC<WithChildrenProp> = (props) => (
  <>props.children</>
)

Код 3.9. Типизация React-компонента

С увеличением опыта работы с библиотеками разработчик узнаёт о предоставляемых ими типах. Так же не следует пренебрегать утильными типами, существующие в самом языке TypeScript.

// имена собираемых в приложении метрик

type MetricsNames = 'total' | 'success' | 'correct' | 'win' | 'fail' | 'error'

// статистика собранных по метрикам действий

type Statistics = Record<MetricNames, number>;

// статистика метрик для получения ачивок (достижений),
// учитываются только некоторые метрики

type AchievementStats = Pick<Statistics, 'total' | 'success' | 'correct' | 'win'>

// условия получения ачивок

type AchievementCondition = Partial<Condition>

Код 3.10 Пример использования утильных типов

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

Следующая статья TypeScript в React-приложениях. 4. Глубокая типизация

Комментарии (0)