Полное руководство по типу Never в TypeScript
Тип never в TypeScript очень мало обсуждается, поскольку он не так распространён и не так неизбежен, как другие типы. Начинающий пользователь TypeScript, вероятно, может игнорировать тип never , поскольку он появляется только при работе с расширенными типами, такими как условные типы, или при чтении их загадочных сообщений об ошибках типов.
Тип never имеет довольно много хороших вариантов использования в TypeScript. Однако у него есть и свои подводные камни , с которыми необходимо быть осторожным.
В этой статье я расскажу о следующем:
- Значение типа never и зачем он нужен.
- Практическое применение и подводные камни типа never .
- Много каламбуров .
Что такое тип never
Чтобы полностью понять, что такое тип never и в чем его назначение, необходимо сначала понять, что это за тип и какую роль он играет в системе типов.
Тип — это набор возможных значений. Например, тип string представляет собой бесконечное множество возможных строк. Поэтому, когда мы аннотируем переменную с типом string , такая переменная может иметь значения только из этого множества, т.е. строки:
let foo: string = 'foo'
foo = 3 // ❌ number не входит в набор строк
В TypeScript never — это пустой набор значений. Фактически, в Flow, другой популярной системе типов JavaScript, эквивалентный тип называется именно empty
Поскольку в наборе нет значений, тип never никогда (каламбурно) не может иметь никакого значения, включая значения типа any . Поэтому never также иногда называют uninhabitable type или bottom type.
declare const any: any
const never: never = any // ❌ Тип 'any' не может быть присвоен типу 'never'
Bottom type — это то, как его определяет TypeScript Handbook. Я обнаружил, что это имеет больше смысла, когда мы помещаем never в дерево иерархии типов — ментальную модель, которую я использую для понимания подтипизации.
Следующий логичный вопрос — зачем нам вообще нужен тип never ?
Зачем нам нужен тип never
Подобно тому, как в системе счисления у нас есть ноль для обозначения количества ничего, в системе типов нам нужен тип для обозначения невозможности.
Само слово невозможность является расплывчатым. В TypeScript невозможность проявляется различными способами, а именно:
- Пустой тип, не имеющий никакого значения, который может быть использован для представления следующего:
- Недопустимые параметры в дженериках и функциях.
- Пересечение несовместимых типов.
- Пустое объединение (объединение типа небытия).
- Не путать с void , так как void означает, что функция не возвращает ничего полезного вызывающему.
const p = Promise.reject('foo') // const p: PromiseКак never работает с объединениями и пересечениями
Аналогично тому, как число ноль работает при сложении и умножении, тип never обладает особыми свойствами при использовании в объединениях и пересечениях типов:
- never выпадает из объединения типов, аналогично тому, как если к числу прибавить ноль, то получится, то же самое число.
- Например, type Res = never | string // string
- Например, type Res = never & string // never
Эти два поведения/характеристики типа never закладывают основу для некоторых наиболее важных вариантов его использования, которые мы рассмотрим далее.
Как использовать тип never
Хотя вы, вероятно, нечасто будете использовать never , существует достаточно много оправданных случаев его применения:
Аннотирование недопустимых параметров функций для наложения ограничений
Поскольку мы никогда не можем присвоить значение типу never , мы можем использовать его для наложения ограничений на функции для различных случаев использования.
Обеспечение исчерпывающего соответствия в операторах switch и if-else
Если функция может принимать только один аргумент типа never , то эта функция никогда не может быть вызвана ни с каким значением, отличным от never (без того, чтобы на нас не накричал компилятор TypeScript):
function fn(input: never) >
// принимает только 'never'
declare let myNever: never
fn(myNever) // ✅
// Передача чего-либо другого (или ничего) приводит к ошибке типа
fn() // ❌ Не предоставлен аргумент 'input'.
fn(1) // ❌ Аргумент типа 'number' не может быть присвоен параметру типа 'never'.
fn('foo') // ❌ Аргумент типа 'string' не может быть присвоен параметру типа 'never'.
// нельзя передать даже `any`.
declare let myAny: any
fn(myAny) // ❌ Аргумент типа 'any' не может быть присвоен параметру типа 'never'.Мы можем использовать такую функцию для обеспечения исчерпывающего соответствия в операторах switch и if-else : используя её как случай по умолчанию, мы гарантируем, что все случаи будут покрыты, поскольку то, что останется, должно быть типа never . Если мы случайно опустим возможное соответствие, то получим ошибку типа. Например:
function unknownColor(x: never): never
throw new Error("unknown color");
>
type Color = 'red' | 'green' | 'blue'
function getColorName(c: Color): string
switch(c)
case 'red':
return 'is red';
case 'green':
return 'is green';
default:
return unknownColor(c); // Аргумент типа 'string' не может быть присвоен параметру типа 'never'
>
>Частичный запрет структурной типизации
Допустим, у нас есть функция, которая принимает параметр либо типа VariantA , либо VariantB . Но пользователь не должен передавать тип, включающий в себя все свойства обоих типов, т.е. подтип обоих типов.
Для параметра мы можем использовать объединение типа VariantA | VariantB . Однако поскольку совместимость типов в TypeScript основана на структурной подтипизации, передача в функцию объектного типа, имеющего больше свойств, чем тип параметра, разрешена (если только не передавать объектные литералы):
type VariantA =
a: string,
>
type VariantB =
b: number,
>
declare function fn(arg: VariantA | VariantB): void
const input = a: 'foo', b: 123 >
fn(input) // TypeScript не жалуется, но для нашего случая это должно быть недопустимоПриведённый фрагмент кода не приводит к ошибке типа в TypeScript.
Используя never , мы можем частично отключить структурную типизацию и не позволить пользователям передавать значения объектов, включающие оба свойства:
type VariantA =
a: string
b?: never
>
type VariantB =
b: number
a?: never
>
declare function fn(arg: VariantA | VariantB): void
const input = a: 'foo', b: 123 >
fn(input) // ❌ Типы свойства 'a' несовместимыПредотвращение непреднамеренного использования API
Допустим, мы хотим создать экземпляр Cache , чтобы читать и сохранять данные из/в него:
type Read = >
type Write = >
declare const toWrite: Write
declare class MyCacheT, R>
put(val: T): boolean;
get(): R;
>
const cache = new MyCacheWrite, Read>()
cache.put(toWrite) // ✅ разрешеноТеперь, по некоторым причинам, мы хотим иметь кэш, доступный только для чтения и позволяющий считывать данные только через метод get . Мы можем указать аргумент метода put как never , чтобы он не мог принять никакое переданное ему значение:
declare class ReadOnlyCacheR> extends MyCachenever, R> >
// Теперь параметр типа `T` внутри MyCache становится `never`.
const readonlyCache = new ReadOnlyCacheRead>()
readonlyCache.put(data) // ❌ Аргумент типа 'Data' не может быть присвоен параметру типа 'never'.Не касаясь типа never , замечу, что это может быть не совсем удачным примером использования производных классов. Я не являюсь экспертом в области объектно-ориентированного программирования, поэтому пользуйтесь собственным мнением.
Обозначим теоретически недостижимые условные ветви
При использовании infer для создания дополнительной переменной типа внутри условного типа необходимо добавить ветвь else для каждого ключевого слова infer :
type A = 'foo';
type B = A extends infer C ? (
C extends 'foo' ? true : false// В этом выражении C представляет A
) : never // эта ветка недостижима, но мы не можем её опуститьЧем полезна эта комбинация extends infer ?
В предыдущей заметке я рассказывал о том, как с помощью extends infer можно создать декларацию локальной (типовой) переменной . Если вы ещё не видели, посмотрите здесь.
Отфильтровать элементы объединения из объединения типов
Помимо обозначения невозможных ветвей, never может использоваться для отсеивания нежелательных типов в условных типах.
Как мы уже говорили, при использовании в качестве элемента объединения тип never удаляется автоматически. Другими словами, тип never бесполезен в объединении типов.
Когда мы пишем утилиту для выбора элементов объединения из объединения типа по определённым критериям, бесполезность типа never в объединениях типов делает его идеальным типом для размещения в ветвях else .
Допустим, нам нужна утилита типа ExtractTypeByName для извлечения элементов объединения, свойство name которых является строковым литералом foo , и отсеивания тех, которые не совпадают:
type Foo =
name: 'foo'
id: number
>
type Bar =
name: 'bar'
id: number
>
type All = Foo | Bar
type ExtractTypeByNameT, G> = T extends name: G> ? T : never
type ExtractedType = ExtractTypeByNameAll, 'foo'> // тип результата - FooПосмотрите, как это работает в деталях:
Далее приведён список шагов, выполняемых TypeScript для оценки и получения результирующего типа:
-
Условные типы распределяются по объединениям типов (в данном случае это Name ):
type ExtractedType = ExtractTypeByNameAll, Name>
⬇️
type ExtractedType = ExtractTypeByNameFoo | Bar, 'foo'>
⬇️
type ExtractedType = ExtractTypeByNameFoo, 'foo'> | ExtractTypeByNameBar, 'foo'>type ExtractedType = Foo extends name: 'foo'> ? Foo : never
| Bar extends name: 'foo'> ? Bar : never
⬇️
type ExtractedType = Foo | nevertype ExtractedType = Foo | never
⬇️
type ExtractedType = FooФильтрация ключей в сопоставленных типах
В TypeScript типы являются неизменяемыми. Если мы хотим удалить свойство из объектного типа, мы должны создать новый тип путём преобразования и фильтрации существующего. Когда мы условно переставляем ключи в сопоставленных типах на never , эти ключи отфильтровываются.
Приведём пример типа Filter , который отфильтровывает свойства объектного типа на основе их типов значений.
type FilterObj extends Object, ValueType> =
[Key in keyof Obj
as ValueType extends Obj[Key] ? Key : never]
: Obj[Key]
>
interface Foo
name: string;
id: number;
>
type Filtered = FilterFoo, string>; //Сужение типов при анализе потока управления
Если мы указываем возвращаемое значение функции как never , это означает, что функция никогда не возвращает управление вызывающей стороне после завершения её выполнения. Мы можем использовать это для анализа потока управления, чтобы сузить круг типов.
Функция может возвращать never по нескольким причинам: она может выбросить исключение на всех путях кода, она может зациклиться навсегда или выйти из программы, например, process.exit в Node.
В следующем фрагменте кода мы используем функцию, возвращающую тип never , чтобы убрать undefined из объединённого типа для foo :
function throwError(): never
throw new Error();
>
let foo: string | undefined;
if (!foo)
throwError();
>
foo; // stringИли вызвать throwError после оператора || или ?? .
let foo: string | undefined;
const guaranteedFoo = foo ?? throwError(); // stringОбозначаем невозможные пересечения несовместимых типов
Этот случай может показаться скорее поведением/характеристикой языка TypeScript, чем практическим применением never . Тем не менее это очень важно для понимания некоторых загадочных сообщений об ошибках, с которыми вы можете столкнуться.
Пересекая несовместимые типы, можно получить тип never .
type Res = number & string // neverА тип never можно получить, пересекая любые типы с never .
type Res = number & never // neverДля объектных типов все сложнее…
При пересечении типов объектов, в зависимости от того, рассматриваются ли непересекающиеся свойства как дискриминантные (в основном это литеральные типы или объединения литеральных типов), можно получить или не получить весь тип, сведённый к never .
В данном примере только свойство name становится never , поскольку string и number не являются дискриминантными свойствами
type Foo =
name: string,
age: number
>
type Bar =
name: number,
age: number
>
type Baz = Foo & Bar //В следующем примере весь тип Baz сводится к never , поскольку boolean является дискриминантным свойством (объединение true | false )
type Foo =
name: boolean,
age: number
>
type Bar =
name: number,
age: number
>
type Baz = Foo & Bar // neverОзнакомьтесь с этим запросом Push Request Reduce intersections by discriminants #36696, чтобы узнать больше.
Как прочитать тип never (из сообщений об ошибках)
Возможно, вы получали сообщения об ошибках, связанных с неожиданным типом never , из кода, который вы не аннотировали never явно. Обычно это происходит потому, что компилятор TypeScript пересекает типы. Он делает это неявно для того, чтобы сохранить безопасность типов и обеспечить их корректность.
Вот пример (поиграйте с ним в TypeScript playground), который я использовал в своей предыдущей статье в блоге о типизации полиморфных функций:
type ReturnTypeByInputType =
int: number
char: string
bool: boolean
>
function getRandomT extends 'char' | 'int' | 'bool'>(
str: T
): ReturnTypeByInputType[T]
if (str === 'int')
// генерируем случайное число
return Math.floor(Math.random() * 10) // ❌ Тип 'number' не может быть присвоен типу 'never'.
> else if (str === 'char')
// генерируем случайный символ
return String.fromCharCode(
97 + Math.floor(Math.random() * 26) // ❌ Тип 'string' не может быть присвоен типу 'never'.
)
> else
// генерируем случайное логическое значение
return Boolean(Math.round(Math.random())) // ❌ Тип 'boolean' не может быть присвоен типу 'never'.
>
>В зависимости от типа передаваемого аргумента функция возвращает либо число, либо строку, либо логическое значение. Для получения соответствующего возвращаемого типа мы используем индексный доступ ReturnTypeByInputType[T] .
Однако для каждого выражения return мы получаем ошибку типа, а именно: Type X is not assignable to type ‘never’ , где X — строка, число или логическое значение, в зависимости от ветви.
Именно здесь TypeScript пытается помочь нам уменьшить вероятность возникновения проблемных состояний в нашей программе: каждому возвращаемому значению должен быть присвоен тип ReturnTypeByInputType[T] (как мы аннотировали в примере), где ReturnTypeByInputType[T] во время выполнения может оказаться либо числом, либо строкой, либо логическим значением.
Типовая безопасность может быть достигнута только в том случае, если мы убедимся, что возвращаемый тип может быть присвоен всем возможным ReturnTypeByInputType[T] , т.е. пересечению чисел, строк и логических значений. А что такое пересечение этих трёх типов? Точно never , поскольку они несовместимы друг с другом. Поэтому в сообщениях об ошибках мы видим never .
Чтобы обойти эту проблему, необходимо использовать утверждения типов (или перегрузки функций):
- return Math.floor(Math.random() * 10) as ReturnTypeByInputType[T]
- return Math.floor(Math.random() * 10) as never
Может быть, другой, более очевидный пример:
function f1(obj: a: number, b: string >, key: 'a' | 'b')
obj[key] = 1; // Тип 'number' не может быть присвоен типу 'never'.
obj[key] = 'x'; // Тип 'string' не может быть присвоен типу 'never'.
>obj[key] может оказаться либо строкой, либо числом в зависимости от значения key во время выполнения программы. Поэтому TypeScript добавил это ограничение, т.е. любые значения, которые мы записываем в obj[key] , должны быть совместимы с обоими типами, string и number , просто, чтобы быть в безопасности. Таким образом, он пересекает оба типа и даёт нам тип never .
Как проверить соответствие типу never
Проверять, является ли тип never , сложнее, чем следовало бы.
Рассмотрим следующий фрагмент кода:
type IsNeverT> = T extends never ? true : false
type Res = IsNevernever> // neverЯвляется ли Res true или false ? Вас может удивить, что ответ — ни то, ни другое: Res на самом деле never . Фактически.
Когда я столкнулся с этим в первый раз, это определённо выбило меня из колеи. Ryan Cavanaugh объяснил это в этом обсуждении. Все сводится к следующему:
- TypeScript автоматически распределяет объединённые типы в условных типах.
- never является пустым объединением.
- Поэтому, когда происходит распределение, распределять уже нечего, поэтому условный тип снова разрешается в never .
Единственным обходным решением является отказ от неявного распределения и обёртывание параметра типа в кортеж:
type IsNeverT> = [T] extends [never] ? true : false;
type Res1 = IsNevernever> // 'true' ✅
type Res2 = IsNevernumber> // 'false' ✅На самом деле это напрямую вытекает из исходного кода TypeScript, и было бы неплохо, если бы TypeScript мог показать это извне.
В заключение
В статье мы рассмотрели достаточно много вопросов:
- Сначала мы поговорили об определении и назначении типа never .
- Затем мы поговорили о различных вариантах его использования:
- наложение ограничений на функции за счёт использования того факта, что never является пустым типом
- отсеивание ненужных членов объединения и свойств объектного типа
- помощь в анализе потока управления
- обозначение недопустимых или недостижимых условных ветвей
Never typescript что за тип
Типизация — это то, почему многие выбирают писать на TypeScript, а не на чистом JavaScript. Хотя на конференциях можно услышать доклады «Почему вам не нужен TS», в больших проектах без него просто не обойтись. А если писать на нём регулярно, то уже даже в небольших проектах обычный JS будет приносить только боль. Я это ощутил, при подготовке уроков по NodeJS для следующего курса, где не используется TypeScript.
Потому важно понимать все особенности типизации. Разберём тип never , который многим, кто знакомится с TypeScript непонятен.
Не путать с void!
Тип void в возвращаемом типе функции, обозначает что функция ничего не возвращает (или по иному говоря возвращает undefined ).
const a = (): void =>
В свою очередь переменной типа never можно присвоить только тип never и никакой другой. Кажется бесполезным? Рассмотрим 3 примера использования:
Исчерпывающие проверки типов
Вот пример функции, которая возвращает тип never :
export const error = (message: string): never =>
На самом деле любая функция, которая кидает ошибку, возвращает этот тип среди других типов. Это можно использовать для исчерпывающих проверок типов.
export const typeRun = (x: string | number): boolean => < if (typeof x === 'string') < return true; >else if (typeof x === 'number') < return false; >return error("!"); >Если бы не использовали never , TS бы выдавал ошибку, так как мы в теории могли бы попасть дальше проверки и вернуть undefined вместо boolean .
Switch / case
Рассмотрим следующий пример:
export type Direction = 'up' | 'down'; export const run = (direction: Direction) => < switch (direction) < case 'up': return 1; case 'down': return -1; default: const a: never = direction; >>Если у нас в Direction добавится новое направление, скажем right , то TypeScript за счёт типа подскажет, что у нас switch больше не валиден, так как при новом значении Direction мы уже можем попасть в default и у нас типу never будет присвоено значение другого типа, что недопустимо.
Conditional types
Благодаря never мы можем исключить не нужные типы. Например, сделаем тип, который если у нас передано значение с number мы присваиваем never . Тем самым мы оставляем только нужный тип в типе А:
type NonNumber = T extends number ? never : T; type A = NonNumber; // A - всегда stringTypeScript: Тип never (возврат из функции)
Тип never используется, когда функция гарантированно ничего не возвращает. Например, если внутри функции есть бесконечный цикл:
function infiniteLoop(): never < while (true) < // Какая-то логика >>Мы явно указали, что функция infiniteLoop ничего не возвращает.
Еще тип never используется, если функция выбрасывает исключение:
function stop(message: string): never
Также тип never используется, когда функция завершает программу:
function exit(code: number = 0): never
Важным условием для never является отсутствие нормального завершения функции. Например, в примере ниже компилятор выдаст ошибку:
// A function returning 'never' cannot have a reachable end point. function printSomething(): never
Функция printSomething() ничего не возвращает явно. Но так как она завершается в принципе, JavaScript подставляет неявный возврат undefined .
never автоматически выводится даже там, где прописан явный возврат. Но компилятор видит, что этот возврат невозможен:
function fail() < // Автоматически выводится never // функция exit, определенная выше, имеет возвращаемый тип never return exit(1); >Задание
Реализуйте функцию fail() , которая выбрасывает любое исключение. Пропишете ее возвращаемый тип явно.
Упражнение не проходит проверку — что делать?
Если вы зашли в тупик, то самое время задать вопрос в «Обсуждениях». Как правильно задать вопрос:
- Обязательно приложите вывод тестов, без него практически невозможно понять что не так, даже если вы покажете свой код. Программисты плохо исполняют код в голове, но по полученной ошибке почти всегда понятно, куда смотреть.
В моей среде код работает, а здесь нет
Тесты устроены таким образом, что они проверяют решение разными способами и на разных данных. Часто решение работает с одними входными данными, но не работает с другими. Чтобы разобраться с этим моментом, изучите вкладку «Тесты» и внимательно посмотрите на вывод ошибок, в котором есть подсказки.
Мой код отличается от решения учителя
Это нормально , в программировании одну задачу можно выполнить множеством способов. Если ваш код прошел проверку, то он соответствует условиям задачи.
В редких случаях бывает, что решение подогнано под тесты, но это видно сразу.
Прочитал урок — ничего не понятно
Создавать обучающие материалы, понятные для всех без исключения, довольно сложно. Мы очень стараемся, но всегда есть что улучшать. Если вы встретили материал, который вам непонятен, опишите проблему в «Обсуждениях». Идеально, если вы сформулируете непонятные моменты в виде вопросов. Обычно нам нужно несколько дней для внесения правок.
Кстати, вы тоже можете участвовать в улучшении курсов: внизу есть ссылка на исходный код уроков, который можно править прямо из браузера.
Как работает never?
Прочел очень много ресурсов про тип never в TS Habr, Medium, SO.en, SO.ru также посмотрел видео курсы. Везде говориться одно и тоже что never используется в бесконечном цикле, в функции где есть бесконечный цикл, ошибка или рекурсия.
function error(message: string): never < throw new Error(message); >function infiniteLoop(): never < while (true) < >> function infiniteRec(): never
В основном везде стандартные тексты. Но я так и не понял, где можно его использовать? Нету рабочего кода где можно потыкать и посмотреть. Попробовал использовать бесконечный цикл с never как написано и не сработало. Не понял почему? И, что не так?
function infiniteLoop(): never < while (true) < console.log("true"); >> infiniteLoop(); // Potential infinite loop: exceeded 10001 iterations.Отслеживать
задан 24 сен 2020 в 13:03
7,026 4 4 золотых знака 29 29 серебряных знаков 62 62 бронзовых знака2 ответа 2
Сортировка: Сброс на вариант по умолчанию
Тип never просто помогает typescript понять что функция никогда ничего не вернёт или всегда выбрасывает ошибку. К примеру в таком коде:
const fn = ()=> < function infiniteLoop(): never < while (true) < console.log("true"); >> infiniteLoop(); return 's' >typescript покажет вам ошибку о недостижимости return ‘s’ благодаря типу never.
Отслеживать
ответ дан 25 сен 2020 в 16:29
876 5 5 серебряных знаков 13 13 бронзовых знаковЯ это понимаю как функция с never не доходит до конца. То есть while не даст дойти к концу и завершить функцию, а throw просто прервет функцию и не даст ей выполнится до конца.
Отслеживать
ответ дан 12 окт 2022 в 16:41
Mini elephant Mini elephant-
Важное на Мете
Связанные
Похожие
Подписаться на ленту
Лента вопроса
Для подписки на ленту скопируйте и вставьте эту ссылку в вашу программу для чтения RSS.
Дизайн сайта / логотип © 2024 Stack Exchange Inc; пользовательские материалы лицензированы в соответствии с CC BY-SA . rev 2024.1.3.2953
Нажимая «Принять все файлы cookie» вы соглашаетесь, что Stack Exchange может хранить файлы cookie на вашем устройстве и раскрывать информацию в соответствии с нашей Политикой в отношении файлов cookie.