- Новые условные типы (Conditional Types)
- Распределительные условные Типы
- Условные типы могут стать заменой перегрузок функций
Универсальные параметры или 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
.