Посмотрим на never с разных сторон?

от автора

Введение

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

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

Документация

В документации never в основном описан в следующих разделах:

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

Пример 1.

export const neverAgainEx1 = () => {   let logValue = ''    const createSomeDesc = (value: string | number | object): string | never=> {     switch(typeof value) {       case 'string':         return 'log string'       case 'number':         return 'log number'       default:         throw new Error('error in createSomeDesc')     }   }    logValue = 'some string' + createSomeDesc({}) }  it('Never again ex1', () => {   expect(() => {     neverAgainEx1()   }).toThrow() }) 

Ключевые моменты:

  • never является подтипом любого типа, поэтому в соответствии с принципом подстановки Барбары Лисков присваивание любому типу never является безопасным:

type TValue = string | never extends string ? true : false // true 
  • функция createSomeDesc выкидывает исключение, если параметр не строка и не число.

  • присваивание нового значения logValue является недостижимой операцией, что очевидно является некорректным поведением

Сам пример показывает, что будет, если тип параметра расширить «забыв» добавить реализацию для object. Тип возвращаемого значения string | never я добавил для наглядности.

Несмотря на то, что такое поведение ts может показаться опасным, оно позволяет свободно использовать код внутри try/catch. При этом, исчерпывающее описание типов описанное в документации становится необходимым для контролирования ситуация на уровне ts.

Выражения типов

Использование never является ключом к созданию типов утилит.

Пример 2

type GenericWithRestriction<T extends string> = T type GenericWithNever<T> = T extends string ? T : never  const neverAgainEx2 = () => {   const value: GenericWithRestriction<string> = ''   //@ts-ignore   const neverValue: GenericWithNever<number> = '' // TS2322: Type string is not assignable to type never   const value2: GenericWithNever<string> = '' } 

Ts «откидывает» любой тип, который может привести к never. Таким образом мы получаем возможность использовать только такие типы, которые имеют смысл.

Примеры использования never в выражениях

Рассмотрим пример:

const messages = {   defaultPrompt: {     ok: 'Ok',     cancel: 'Cancel'   },   defaultAction: {     file: {       rm: 'delete file',       create: 'create file'     },     directory: {       rm: 'delete directory',       create: 'make directory'     }   },   title1: 'default title 1', }  export const getMessageByKey = (key: string): string => eval(`messages.${key}`) 

Задача: настроить тип getMessageByKey так, что бы в key были строки вида path.to.value. Реализация в данном случае значения не имеет.

Сам message превратим в литерал через as const

Вариант 1:

type KeyTree = {   [key: string]: string | KeyTree, }  type TExtractAllKeysTypeA<O extends KeyTree, K extends keyof O = keyof O> = K extends string   ? O[K] extends KeyTree     ? `${K}.${TExtractAllKeysTypeA<O[K]>}`     : K   : never 

Ключевым моменты:

  • K extends string выполняет две функции

    1. Позволяет работать дистрибутивности объединения относительно операции extends

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

  • Для задания ключей вида path.to.property используем шаблонные строчные литералы

  • Для создания множества всех ключей используем рекурсию

  • Для простоты использования второму дженерику задаем дефолтное значение

В данном случае явное использование never играет скромную роль, отсекая symbol из множества ключей keyof O. Но есть и неявное поведение. При значениях ключей отличных от string | KeyTree, выражение ${K}.${TExtractAllKeysTypeA<O[K]>} будет приведено к never и тогда такие ключи будут откинуты. А саму утилиту можно преобразовать к виду:

type TExtractAllKeysTypeA<O, K extends keyof O = keyof O> = K extends string   ? O[K] extends string     ? K     : `${K}.${TExtractAllKeysTypeA<O[K]>}`   : never 

Разумеется в этом случае литерал messages никак не контролируется.

Итоговый результат:

export const getMessageByKey = (key: TExtractAllKeysTypeA<typeof messages>): string => eval(`messages.${key}`) 

Вариант 2:

type TExtractAllKeysTypeB<O> = {   [K in keyof O]: K extends string     ? O[K] extends string       ? K       : `${K}.${TExtractAllKeysTypeB<O[K]>}`     : never }[keyof O] 
  • количество дженериков сократилось до одного

  • never используется более изобретательным способом. ТС откидывает свойства, значения которых never

  • используется неявное приведение к never

И в конце можно рассмотреть функцию, которая работает с любым messages

const _getMessageByKeyTypeA = <T extends KeyTree>(data: T) => {   return (key: TExtractAllKeysTypeA<T>): string => eval(`data.${String(key)}`) }  const _getMessageByKeyTypeB = <T>(data: T) => {   return (key: TExtractAllKeysTypeB<T>): string => eval(`data.${String(key)}`) }  export const getMessageByKeyTypeA = _getMessageByKeyTypeA(messages) export const getMessageByKeyTypeB = _getMessageByKeyTypeB(messages) 

Вместо заключения

Как видно из этой заметки освоить never является необходимым не только для того, что бы не пропустить ситуации в которых данный тип нужен «по прямому назначению», но и для того, что бы освоить создание типов-утилит.


ссылка на оригинал статьи https://habr.com/ru/articles/849074/


Комментарии

Добавить комментарий

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