Yandex.Metrika Counter

Обсудить проект

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

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

Огромная благодарность за составление и помощь в подготовке материала разработчикам Дмитрию Бердникову и Александру Инкееву!

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

Получение any вместо unknown

Если используем тип any, то теряем типизацию — мы можем обратиться к любому методу или свойству такого объекта, и компилятор не предупредит нас о возможных ошибках. Если же мы используем unknown, то компилятор подскажет о них.

Некоторые функции и операции возвращают any по умолчанию — это не совсем очевидно, вот несколько примеров:


// JSON.parse
const a = JSON.parse('{ a: 1 }'); // any
// Array.isArray
function parse(a: unknown) {
if (Array.isArray(a)) {
console.log(a); // a[any]
}
}
// fetch
fetch("/")
.then((res) => res.json())
.then((json) => {
console.log(json); // any
});
// localStorage, sessionStorage
const b = localStorage.a; // any
const c = sessionStorage.b // any

Эту проблему может решить ts-reset

ts-reset – это библиотека, которая помогает решить некоторые неочевидные моменты, когда хотелось бы, чтобы TypeScript по умолчанию работал иначе.

Методы массивов слишком строгие для конструкции as const

Также это встречается в методах has у Set и Map.

Пример: создаем массив пользователей, присваиваем конструкцию as const, затем вызываем метод includes и получаем ошибку, потому что аргумент 4 не существует в типе userIds.


const userIds = [1, 2, 3] as const;

userIds.includes(4);

Избавиться от ошибки также поможет использование ts-reset.

Отфильтровать массив от undefined

Предположим, у нас есть какой-то числовой массив, в котором может быть undefined. Чтобы избавиться от этих undefined, отфильтруем массив. Но массив newArr всё равно будет содержать тип массива number или undefined.


const arr = [1, 2, undefined];
const newArr = arr.filter((item) => item !== undefined);

Решить проблему можно следующим образом, тогда newArr2 будет иметь тип number:


const newArr2 = arr.filter((item): item is number => item !== undefined);

Также ts-reset может помочь, но только для случая, когда аргумент функции filter является типом BooleanConstructor.


const filteredArray = [1, 2, undefined].filter(Boolean)

Сужение типа с помощью скобочной нотации

Создаем объект с типом ключ строка, значение строка или массив строк. 

Затем обращаемся к свойству объекта, используя скобочную нотацию и проверяем, что тип возвращаемого значения объекта является строкой. В typescript версии ниже 4.7 тип queryCountry будет строкой или массивом строк, т.е. автоматическое сужение типов не работает, хотя мы уже проверили условием. 

Если же использовать typescript версии 4.7 и выше, сужение типа будет работать так, как мы этого ожидаем.


const query: Record<string, string | string[]> = {};

const COUNTRY_KEY = 'country';

if (typeof query[COUNTRY_KEY] === 'string') {
    const queryCountry: string = query[COUNTRY_KEY];
}

Ссылка на документацию

Проблемы enum

Создаем enum и явно не указываем значения, тогда у каждого ключа по порядку будут числовые значения от 0 и далее.

С помощью этого enum затипизируем первый аргумент функции showMessage, в ожидании, что мы сможем передать только те коды, которые описаны в enum:


enum LogLevel {
    Debug, // 0
    Log, // 1
    Warning, // 2
    Error // 3
}

const showMessage = (logLevel: LogLevel, message: string) => {
    // code...
}

showMessage(0, 'debug message');
showMessage(2, 'warning message');

Если же передать не содержащееся в enum значение в качестве аргумента, мы должны увидеть ошибку "Argument of type '-100' is not assignable to parameter of type 'LogLevel'". Но в typescript ниже версии 5.0 такой ошибки нет, хотя по логике она должна быть: 


showMessage(-100, 'any message')

Также мы можем создать enum и явно указать числовые значения. Константе a указываем тип enum и присваиваем любое несуществующее число, которого нет в enum, например, 1. При использовании TS ниже 5 версии ошибки не будет.


enum SomeEvenDigit {
    Zero = 0,
    Two = 2,
    Four = 4
}

const a: SomeEvenDigit = 1;

И еще момент: при использовании TypeScript ниже 5 версии, вычисляемые значения не могут быть использованы в enum.


enum User {
  name = 'name',
    userName = `user${User.name}`
}

Ссылка на документацию.

Функции, у которых явно указан возвращаемый тип undefined, должны иметь явный возврат

В версиях TypeScript ниже 5.1 будет появляться ошибка в случаях, когда у функции явно указан тип undefined, но нет return.


function f4(): undefined {}

Ошибки не будет в следующих случаях:


function f1() {}

function f2(): void {}

function f3(): any {}

Закрепим. Если явным образом присвоить функции тип void или any, ошибки не будет. Она появится, если присвоить функции тип undefined, и только при использовании TypeScript версии ниже 5.1.

Ссылка на документацию.

Поведение enum’ов соответствует номинативной типизации, а не структурной

И это несмотря на то, что у TypeScript типизация, наоборот, структурная.

Создадим enum и функцию, аргумент которой типизируем этим enum. Попробуем вызвать функцию, передав в качестве значения этого аргумента строку, которая идентична одному из значений enum. Получаем ошибку в showMessage тип аргумента 'Debug' не может быть присвоен, так как ожидается тип enum 'LogLevel'.


enum LogLevel {
    Debug = 'Debug',
    Error = 'Error'
}

const showMessage = (logLevel: LogLevel, message: string) => {
    // code...
}

showMessage('Debug', 'some text')

Даже если мы создадим новый enum с такими же значениями, это не сработает.


enum LogLevel2 {
    Debug = 'Debug',
    Error = 'Error'
}
showMessage(LogLevel2.Debug, 'some text')

Решение – использовать объекты со значением as const.


const LOG_LEVEL = {
    DEBUG: 'debug',
    ERROR: 'error'
} as const

type ObjectValues = T[keyof T]

type LogLevel = ObjectValues;

const logMessage = (logLevel: LogLevel, message: string) => {
    // code...
}

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


logMessage('debug', 'some text')
logMessage(LOG_LEVEL.DEBUG, 'some text')

Возможность возврата неправильного типа данных в функции с перегрузкой

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


function add(x: string, y: string): string
function add(x: number, y: number): number
function add(x: unknown, y: unknown): unknown {

    if (typeof x === 'string' && typeof y === 'string') {
                return 100;
    }

    if (typeof x === 'number' && typeof y === 'number') {
        return x + y
    }

    throw new Error('invalid arguments passed');
}

Далее ожидаем, что const будет содержать тип string, но получаем число.


const str = add("Hello", "World!");
const num = add(10, 20);

 

Передача объекта как аргумент функции с лишним свойством

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

Однако в typescript возможно нарушить это правило:


type Func = () => {
  id: string;
};

const func: Func = () => {
  return {
    id: "123",
    name: "Hello!",
  };
};

Для большей наглядности, создадим объект с настройками formatAmountParams, который передадим в функцию formatAmount. Как можно увидеть, объект с настройками может содержать лишние свойства и ошибки никакой не будет.


type FormatAmount = {
  currencySymbol?: string,
  value: number
}

const formatAmount = ({ currencySymbol = '$', value }: FormatAmount) => {
  return `${currencySymbol} ${value}`;
}

const formatAmountParams = {
  currencySymbol: 'USD',
  value: 10,
  anotherValue: 20
}

Нет ошибки, если передаем объект, который содержит лишние свойства: 


formatAmount(formatAmountParams);

Получим ошибку, если создадим объект как аргумент функции и передадим с лишним свойством.


formatAmount({ currencySymbol: '', value: 10, anotherValue: 12 });

Можем столкнуться с неочевидным поведением, если захотим переименовать currencySymbol на currencySign.

Сначала изменим в типе, затем typescript подскажет, что надо изменить ключ в объекте с  currencySymbol на currencySign.


type FormatAmount = {
  currencySign?: string,
  value: number
}

const formatAmount = ({ currencySign = '$', value }: FormatAmount) => {
  return `${currencySign} ${value}`;
}

const formatAmountParams = {
  currencySymbol: 'USD',
  value: 10
}

formatAmount(formatAmountParams);

Ошибок нет – можно подумать, что рефакторинг прошел без проблем. Но в formatAmountParams осталось старое название currencySymbol и вместо ожидаемого результата 'USD 10' мы получим $10'. 

Потеря типизации при использовании Object.keys

Создадим объект obj. С помощью Object.keys создадим массив с ключами объекта и проитерируемся по этому массиву. Если в цикле обратимся к объекту по ключу, typescript скажет, что не можем этого сделать, так как общий тип 'string' не может быть использован в качестве ключа для объекта obj.

Возможное решение – скастовать тип с помощью конструкции as. Но это может быть небезопасно, потому что мы вручную устанавливаем, какой тип там будет находиться. Нужно привести к тому, чтобы [key] был не просто строкой, а ключом, и явно это указать.


const obj = {a: 1, b: 2}

Object.keys(obj).forEach((key) => {
  console.log(obj[key])
  console.log(key as keyof typeof obj)
});

 

TypeScript может не распознать изменение типа данных

Создадим тип UserMetadata, как Map ключ-значение. На основе этого типа создаём cache и пытаемся получить значение по ключу 'foo' с помощью метода get. Всё работает как ожидается.

Затем создадим объект cacheCopy на основе cache. И также вызываем метод get. Typescript не подскажет, что что-то не так, но будет ошибка, так как у объекта нет метода get.


type Metadata = {};

type UserMetadata = Map<string, Metadata>;

const cache: UserMetadata = new Map();

console.log(cache.get('foo'));

const cacheCopy: UserMetadata = { ...cache };

console.log(cacheCopy.get('foo'));

 

Мерж интерфейсов

Интерфейсы в отличии от типов могут мержиться. Если в одном файле будут интерфейсы с одинаковыми названиями, то когда мы назначим этот интерфейс он будет содержать свойства из всех интерфейсов с одинаковыми названиями.


interface User {
    id: number;
}

interface User {
    name: string;
}

// Error: Property 'id' is missing in type '{ name: string; }' but required in type 'User', because User interfaces merged
const user: User = {
    name: 'bar',
}

Более того, если у нас есть глобальные интерфейсы, например, предопределенные в самом typescript, они также смержатся. Например, если создадим интерфейс с именем comment, получим мерж интерфейсов, потому что comment уже существует в lib.dom.d.ts.


interface Comment {
  id: number;
  text: string;
}

// Error: Type '{ id: number; text: string; }' is missing the following properties from type 'Comment': data, length, ownerDocument, appendData, and 59 more.
const comment: Comment = {
  id: 5,
  text: "good video!",
};

Ссылка на документацию.

Еще полезное

Если вам хочется закрепить информацию по теме, но не хочется перечитывать статью, можно посмотреть несколько роликов на youtube: