Как я могу избежать дублирования ключа в значении и дублирования определений типа и константы без использования ручных утверждений типа?

Рассмотрим следующий код TypeScript (ссылка на код):

type Name = 'foo' | 'bar'

type ProviderFuncFromString = (data: string) => { name: Name; data: string }

type ProviderFuncFromNumber = (numb: number) => { name: Name; data: string }

function providerFuncFromStringFactory(name: Name): ProviderFuncFromString {
  return (data) => ({ name, data })
}

function providerFuncFromNumberFactory(name: Name): ProviderFuncFromNumber {
  return (numb) => ({ name, data: numb.toString() })
}

type ProviderMap = {
  foo: ProviderFuncFromString
  bar: ProviderFuncFromNumber
}

const providerMap: ProviderMap = {
  foo: providerFuncFromStringFactory('foo'),
  bar: providerFuncFromNumberFactory('bar'),
}

Здесь есть два дубля:

  • И ProviderMap, и providerMap говорят, что функция поставщика для foo является «из строки», а функция поставщика для bar — «из числа».
  • В providerMap ключи foo и bar дублируются как по ключу, так и по значению.

Как я могу удалить оба этих дублирования?

Я не хочу использовать какие-либо утверждения типа вручную, такие как ключевое слово as, поскольку я хочу поддерживать безопасность типов, обеспечиваемую TypeScript.

Я знаю, что для простых типов объединения строк я могу дедуплицировать type и const следующим образом:

export const NameVal = ['foo', 'bar']

export type NameType = (typeof NameVal)[number]

Здесь foo и bar определяются только один раз, предоставляя нам как type, так и время выполнения const.

Однако я не знаю, как применить этот «трюк» к более продвинутым типам.

Я пробовал, например:

type providerMapType = (typeof providerMap)[keyof typeof providerMap]

но тогда тип становится

type providerMapType = ProviderFuncFromString | ProviderFuncFromNumber

и не желаемый

type providerMapType = {
  foo: ProviderFuncFromString
  bar: ProviderFuncFromNumber
}

Что касается дублирования внутри providerMapfoo и bar, я пробовал что-то вроде этого:


function getFromStringFactory(name: Name): [Name, ProviderFuncFromString] {
  return [name, providerFuncFromStringFactory(name)]
}

function getFromNumberFactory(name: Name): [Name, ProviderFuncFromNumber] {
  return [name, providerFuncFromNumberFactory(name)]
}

const providerMap2 = Object.fromEntries([
  getFromStringFactory('foo'),
  getFromNumberFactory('bar'),
])

но здесь providerMap2 оценивается как const providerMap2: any.

если я правильно понимаю суть, вы хотите, чтобы тип возвращаемого значения функции выводился и распространялся на дальнейший код, верно? если это так, я бы посмотрел что-нибудь вроде библиотеки Zod (github.com/colinhacks/zod). Определить схему; используйте его для анализа возвращаемого значения return mySchema.parse(val); таким образом lsp TS сможет определить тип возвращаемого значения, поскольку он сообщит TS, что возвращаемое значение имеет тип, определенный схемой, или выдаст ошибку. Еще одним преимуществом является то, что схемы находятся во время выполнения, поэтому вы также обнаружите ошибки времени выполнения, что делает TS почти безопасным для типов.

liminor 12.07.2024 09:53

@liminor спасибо за рекомендацию, я присмотрюсь к Зоду. Мой вариант использования: у меня есть строка, определяющая, какое «действие на бэкэнде» я хочу выполнить, и я хочу иметь строго типизированную карту, которая сообщает мне: «Для действия foo это функция, которую вы должны вызвать, чтобы получить полезную нагрузку тела json. для внутреннего вызова». Я хочу определить эту связь только один раз в исходном коде. Если однажды я добавлю qux в список допустимых действий, я хочу, чтобы компилятор TS помог мне обновить весь соответствующий код. Я не хочу дублирования или утверждений типов вручную, чтобы избежать ошибок и переработок.

Konrad Jamrozik 12.07.2024 10:00

Ах я вижу. это немного отличается от того, что я изначально думал, хотя Zod позволит возвращаемым типам распространяться через вызовы API. Я думаю, что минимальный воспроизводимый репо/jsfiddle/etc может помочь, чтобы мы могли с ним поиграть. мне нравится концепция определения действий с помощью карты. может быть более кратким, чем наличие оператора switch или безбожного количества операторов if

liminor 12.07.2024 10:19

@liminor спасибо за предложение. Я добавил ссылку на код в начало своего сообщения.

Konrad Jamrozik 12.07.2024 10:45

вам поможет переворот сценария: const providerMap = { foo: providerFuncFromStringFactory('foo'), bar: providerFuncFromNumberFactory('bar'), }; type ProviderMap = typeof providerMap. насколько я понимаю, вы пишете схему без библиотеки проверки :) так почему бы сначала не определить карту, а затем определить тип карты?

liminor 12.07.2024 12:40
codepen.io/kolchurinvv/pen/OJeVbqR вроде работает. Основная проблема этой реализации заключается в том, что чем больше видов действий у вас есть, тем больше будет расти тернарный оператор для типа ActionToFunctionMap. в результате поставщикМап может иметь только имена свойств типа Имя, а аргументом поставщикаFuncFrom....() может быть только имя этого свойства. кроме того, действия типизируются на основе ActionToFunctionMap
liminor 12.07.2024 14:46

подумал, что, полагаю, это не помогло написать меньше кода :)

liminor 12.07.2024 14:52

@jcalz, твое решение великолепно. Я поддержу ваш ответ. Действительно, вы используете as, но, как вы говорите, это на самом деле не снижает защиту от ввода, если я правильно понимаю. Однако есть еще одна проблема. С вашим решением я могу сделать это: providerMap.foo('bar'). Я хотел убедиться, что у меня есть providerMap.foo, что эквивалентно providerMap.foo('foo'). Я хочу обеспечить это без необходимости дважды писать foo в определениях, например foo: providerFuncFromStringFactory('foo') или что-то подобное.

Konrad Jamrozik 12.07.2024 20:39

@liminor, ваше решение тоже великолепно, и я бы также поддержал ваш ответ. Строго говоря, вы дублируете использование имен в своем исходном коде, как здесь: bar: providerFuncFromNumberFactory('bar'),, я заметил, что компиляция не удастся, если я попытаюсь заменить bar на buz (что вы объяснили). Это то, что меня волнует больше всего — я согласен с дублированием bar, пока компилятор сообщает мне, что я допустил ошибку. Знает ли компилятор, что мне нужно использовать именно bar из-за оператора extends? Дайте мне знать, хотите ли вы написать ответ, чтобы я мог проголосовать, или мне следует это сделать.

Konrad Jamrozik 12.07.2024 20:52

@jcalz извини, позволь мне уточнить мои намерения. Мне нужна карта, на которой написано: «Для ключа foo сопоставьте функцию, которая возвращает объект, к которому прикреплен ключ name к foo». Если я это сделаю providerMap.foo('bar'), то я верну функцию, которая присоединяет name к bar, но эта функция была связана с foo на карте — это ошибка! Еще я хочу, чтобы эта карта точно сообщала мне сигнатуру функции для каждой клавиши. Итак, там сказано: ключ foo сопоставляется с функцией, которая принимает строку. bar в функцию, которая принимает число и т. д. Ключ карты и возвращаемое значение ключа name объекта должны совпадать.

Konrad Jamrozik 12.07.2024 22:00

@jcalz , чтобы добавить : обратите внимание, что на игровой площадке TS вы поделились картами providerMap с фабриками, такими как foo: providerFuncFromStringFactory, но в моем исходном фрагменте кода из моего вопроса я хочу сопоставить частично примененные фабрики, например foo: providerFuncFromStringFactory('foo').

Konrad Jamrozik 12.07.2024 22:02

@jcalz извини, ты абсолютно прав. И ваше решение отвечает всем моим требованиям, спасибо! Почему-то я не заметил, чтобы вы использовали mappy( в этой строке: const providerMap = mappy({. Из-за этого я думал, что если я сделаю console.info(providerMap.foo('bar')), то получу ProviderFuncFromString с Name, связанным с bar, но на самом деле я получаю {"name": "foo", "data": "bar"}, что является желаемым поведением. Еще раз спасибо за ответ и терпение!

Konrad Jamrozik 12.07.2024 22:35

Ладно, спасибо, я думал, что схожу с ума. Итак, резервное копирование... это всего лишь реализация mappy, которая не проверена компилятором как безопасная (из-за as внутри нее), но у пользователей mappy такой проблемы нет. А реализация mappy заботится о дедупликации, поскольку она использует ключ входного объекта как ключ и аргумент. В любом случае, я напишу ответ, когда у меня будет возможность.

jcalz 12.07.2024 22:47

Поскольку у вас есть ответ, который мне нравится больше, чем мой, я не буду его писать. Ключевое слово extends по сути означает «x является частью y». В случае литеральных значений, таких как «foo», единственным значением x может быть «foo».

liminor 13.07.2024 21:43
Зод: сила проверки и преобразования данных
Зод: сила проверки и преобразования данных
Сегодня я хочу познакомить вас с библиотекой Zod и раскрыть некоторые ее особенности, например, возможности валидации и трансформации данных, а также...
Как заставить Remix работать с Mantine и Cloudflare Pages/Workers
Как заставить Remix работать с Mantine и Cloudflare Pages/Workers
Мне нравится библиотека Mantine Component , но заставить ее работать без проблем с Remix бывает непросто.
Угловой продивер
Угловой продивер
Оригинал этой статьи на турецком языке. ChatGPT используется только для перевода на английский язык.
TypeScript против JavaScript
TypeScript против JavaScript
TypeScript vs JavaScript - в чем различия и какой из них выбрать?
Синхронизация localStorage в масштабах всего приложения с помощью пользовательского реактивного хука useLocalStorage
Синхронизация localStorage в масштабах всего приложения с помощью пользовательского реактивного хука useLocalStorage
Не все нужно хранить на стороне сервера. Иногда все, что вам нужно, это постоянное хранилище на стороне клиента для хранения уникальных для клиента...
Что такое ленивая загрузка в Angular и как ее применять
Что такое ленивая загрузка в Angular и как ее применять
Ленивая загрузка - это техника, используемая в Angular для повышения производительности приложения путем загрузки модулей только тогда, когда они...
2
14
76
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Чтобы полностью прекратить дублирование ключей, вам нужно сделать что-то вроде

const providerMap = Object.fromEntries(Object.entries({
    foo: providerFuncFromStringFactory,
    bar: providerFuncFromNumberFactory
}).map(([k, f]) => [k, f(k)]));

используя Object.entries() , чтобы взять объект фабрик и создать массив записей его свойств, затем сопоставить эти записи так, чтобы фабрики вызывались с их ключом в качестве аргумента, а затем использовать Object.fromEntries(), чтобы повторно собрать это в объект.

И это прекрасно работает в JavaScript, но TypeScript не следует этой логике. Object.entries(), Object.fromEntries() и общие map() недостаточно строго типизированы в TypeScript, чтобы это работало. В Stack Overflow есть различные вопросы и ответы по этим проблемам, такие как Вывод формы результата Object.fromEntries() в TypeScript и Попытка получить правильный тип fromEntries и Сопоставление значений, типизированных кортежем, с разными значение типа кортежа без приведения.

В настоящее время вам придется где-то использовать утверждения типа . Лучшее, что вы можете сделать, — это ограничить их внутри некоторой функции, которую вы собираетесь написать только один раз, чтобы вызывающие функцию могли использовать ее, не беспокоясь.

Вот так, возможно:

function mapCallWithKeyAsArgument<T extends object>(
    funcs: { [K in keyof T]: (name: K) => T[K] }
) {
    return Object.fromEntries(
        Object.entries(funcs as { [k: string]: Function })
            .map(([k, f]) => [k, f(k)])
    ) as T
}

Здесь mapCallWithKeyAsArgument() принимает объект функции, где каждая функция принимает аргумент типа ключа, и выдает нужное значение в нашем выходном свойстве. Выходные данные имеют общий тип T, а входные funcs имеют сопоставленный тип { [K in keyof T]: (name: K) => T[K] }. Когда вы его вызываете, TypeScript выводит T из типа funcs в процессе, называемом «выводом из сопоставленных типов» (по какой-то причине он задокументирован только в v1 руководства TS).

Внутри mapCallWithKeyAsArgument() я использую утверждения типа дважды: одно позволяет TypeScript считать, что каждый член funcs является типом функции, а другое требует от TypeScript принять, что тип вывода является желаемым T выводом. Опять же, компилятор не проверяет это как безопасное; вместо этого нам придется делать это самим.

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

const providerMap = mapCallWithKeyAsArgument({
    foo: providerFuncFromStringFactory,
    bar: providerFuncFromNumberFactory,
});
type ProviderMap = typeof providerMap;
/* type ProviderMap = {
    foo: ProviderFuncFromString;
    bar: ProviderFuncFromNumber;
} */

Здесь providerMap имеет свойство foo типа ProviderFuncFromString и свойство bar типа ProviderFuncFromNumber по желанию!


Обратите внимание, что мы можем использовать оператор типа typeof для получения типа ProviderMap из значения providerMap, поэтому нам не нужно дублировать foo и bar там.

Детская площадка, ссылка на код

Другие вопросы по теме