Главная Категории Контакты Поиск

Условные типы в TypeScript

Как динамически распределять типы функций с условными типами в TypeScript.

TypeScript·26.11.2019·читать 3 мин 🤓·Автор: Alexey Myzgin

Универсальные параметры или Generics всегда были хорошим решением для создания многоразовых типов. Мы можем установить тип <T> и вернуть совершенно новый тип - это как функция.

type Item<T> = {
    id: T,
    container: any;
}

Новые условные типы (Conditional Types)

Мы можем “сказать” компилятору анализировать тернарные (ternary) выражения.

interface StringContainer {
    value: string;
    format(): string;
    split(): string[];
}

interface NumberContainer {
    value: number;
    nearestPrime: number;
    round(): number;
}

type Item<T> = {
    id: T,
    container: T extends string ? StringContainer : NumberContainer;
}

Если наш параметр <T> является строкой - наше свойство container будет иметь тип StringContainer. В противном случае он будет NumberContainer.

Давай создадим экземпляр Item и передадим строку в качестве универсального параметра.

const item: Item<string> = {
    id: 'wqe',
    container: null
}

Если мы попытаемся получить доступ к контейнеру item.container., то увидим format, split которые являются свойствами StringContainer.

interface StringContainer {
    value: string;
    format(): string;
    split(): string[];
}

interface NumberContainer {
    value: number;
    nearestPrime: number;
    round(): number;
}

type Item<T> = {
    id: T,
    container: T extends string ? StringContainer : NumberContainer;
}

const item: Item<string> = {
    id: 'wqe',
    container: null
}

item.container. // format, split, value

Теперь давай попробуем использовать число вместо строки.

const item: Item<number> = {
    id: 5,
    container: null
}

item.container. // nearestPrime, round, value

Как и ожидалось, мы получили nearestPrime,round - это свойства NumberContainer.

Распределительные условные Типы

Условными типами, которым в качестве аргумента типа, устанавливается объединенный тип (Union Type) - называются распределительные условные типы (Distributive Conditional Types).

Мы можем создать type для фильтра массивов ArrayFilter. Всё, что мы должны “сказать”, это то, что если универсальным параметром <T> является массив, мы вернем этот тип. В противном случае мы будем возвращать тип never, чтобы компилятор мог его игнорировать.

type ArrayFilter<T> = T extends any[] ? T : never;

Создаем новый тип StringsOrNumbers, используем тип ArrayFilter который мы создали ранее, и передадим в него тип который может быть <string | number | string[] | number[]>.

type ArrayFilter<T> = T extends any[] ? T : never;

type StringsOrNumbers = ArrayFilter<string | number | string[] | number[]>;

Теперь, если мы наведем курсор мыши на наш новый тип StringsOrNumbers, то увидим, что это либо массив строк, либо массив чисел type StringsOrNumbers = string[] | number[]. Всё, что не относится к массиву типов было отфильтровано.

Всё это работает из-за двух механизмов.

  • Условные типы распространяются на каждый элемент и набор возможных типов, который мы передаем в него - <string | number | string[] | number[]>. Таким образом, в этом случае он будет применять тернарные операции для каждого из этих четырех типов по отдельности и заменять каждый из них тем, что будет возвращает выражение T extends any[] ? T : never. В данном случае он вернет never | never | string[] | number[].

  • Во-вторых, потому что по определению тип never никогда не может произойти. Если typescript видит его в объединении типов, как у нас never | never | string[] | number[], never будет игнорироваться. В итоге мы получим string[] | number[].

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

У нас есть тип IItemService, который имеет функцию getItem.

interface Book {
    id: string;
    tableOfContents: string[];
}

interface Movie {
    id: number;
    diagonal: number;
}

interface IItemService {
    getItem<T>(id: T): Book | Movie;
}

const itemService: IItemService;

Нам нужно сделать эту функцию универсальной. Если id будет строкой, она вернет интерфейс Book, (книги индексируются по строке) или, если id число, то будет возвращает интерфейс Movie.

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

interface IItemService {
    getItem(id: string): Book;
    getItem(id: number): Movie;
    getItem<T>(id: T): Book | Movie;
}

Давай посмотрим, как мы можем использовать условные типы и оставить только одно определение.

Мы можем просто добавить условие, если T - строка, вернуть Book; в противном случае вернуть Movie.

interface IItemService {
    getItem<T>(id: T): T extends string ? Book : Movie;
}

Итак, представим, что переменная itemService инициализирована и имеет тип IItemService. Мы попытаемся получить книгу book, передав строку, а также получить фильм movie, передав число.

const itemService: IItemService;

const book = itemService.getItem('5');
const movie = itemService.getItem(5);

Если мы наведем курсор мышки на book, typescript сообщить нам, что она имеет тип Book. Если мы наведем курсор на переменную movie, typescript сообщить нам, что это тип Movie.

interface Book {
    id: string;
    tableOfContents: string[];
}

interface Movie {
    id: number;
    diagonal: number;
}

interface IItemService {
    getItem<T>(id: T): T extends string ? Book : Movie;
}

const itemService: IItemService;

const book = itemService.getItem('5'); // const book: Book
const movie = itemService.getItem(5); // const movie: Movie

Проблема в этом подходе состоит в том, что, если мы укажем логическое значение boolean:

const movie = itemService.getItem(true); // const movie: Movie

typescript всё равно будет думать что movie это тип Movie. Чтобы этого не случалось, мы можем просто заблокировать возможные типы идентификатора, который может быть либо строкой, либо числом string | number.

interface IItemService {
    getItem<T extends string | number>(id: T): T extends string ? Book : Movie;
}

После того как мы это сделаем, typescript начнёт “жаловаться” Argument of type 'true' is not assignable to parameter of type 'string | number'. Таким образом мы сделали так что функция getItem принимает либо строку, либо числом string | number.

Website, name & logo
Copyright © 2022. Alex Myzgin