Рассмотрим следующий код 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
}
Что касается дублирования внутри providerMap
foo
и 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
.
@liminor спасибо за рекомендацию, я присмотрюсь к Зоду. Мой вариант использования: у меня есть строка, определяющая, какое «действие на бэкэнде» я хочу выполнить, и я хочу иметь строго типизированную карту, которая сообщает мне: «Для действия foo
это функция, которую вы должны вызвать, чтобы получить полезную нагрузку тела json. для внутреннего вызова». Я хочу определить эту связь только один раз в исходном коде. Если однажды я добавлю qux
в список допустимых действий, я хочу, чтобы компилятор TS помог мне обновить весь соответствующий код. Я не хочу дублирования или утверждений типов вручную, чтобы избежать ошибок и переработок.
Ах я вижу. это немного отличается от того, что я изначально думал, хотя Zod позволит возвращаемым типам распространяться через вызовы API. Я думаю, что минимальный воспроизводимый репо/jsfiddle/etc может помочь, чтобы мы могли с ним поиграть. мне нравится концепция определения действий с помощью карты. может быть более кратким, чем наличие оператора switch или безбожного количества операторов if
@liminor спасибо за предложение. Я добавил ссылку на код в начало своего сообщения.
вам поможет переворот сценария: const providerMap = { foo: providerFuncFromStringFactory('foo'), bar: providerFuncFromNumberFactory('bar'), }; type ProviderMap = typeof providerMap
. насколько я понимаю, вы пишете схему без библиотеки проверки :) так почему бы сначала не определить карту, а затем определить тип карты?
подумал, что, полагаю, это не помогло написать меньше кода :)
@jcalz, твое решение великолепно. Я поддержу ваш ответ. Действительно, вы используете as
, но, как вы говорите, это на самом деле не снижает защиту от ввода, если я правильно понимаю. Однако есть еще одна проблема. С вашим решением я могу сделать это: providerMap.foo('bar')
. Я хотел убедиться, что у меня есть providerMap.foo
, что эквивалентно providerMap.foo('foo')
. Я хочу обеспечить это без необходимости дважды писать foo
в определениях, например foo: providerFuncFromStringFactory('foo')
или что-то подобное.
@liminor, ваше решение тоже великолепно, и я бы также поддержал ваш ответ. Строго говоря, вы дублируете использование имен в своем исходном коде, как здесь: bar: providerFuncFromNumberFactory('bar'),
, я заметил, что компиляция не удастся, если я попытаюсь заменить bar
на buz
(что вы объяснили). Это то, что меня волнует больше всего — я согласен с дублированием bar
, пока компилятор сообщает мне, что я допустил ошибку. Знает ли компилятор, что мне нужно использовать именно bar
из-за оператора extends
? Дайте мне знать, хотите ли вы написать ответ, чтобы я мог проголосовать, или мне следует это сделать.
@jcalz извини, позволь мне уточнить мои намерения. Мне нужна карта, на которой написано: «Для ключа foo
сопоставьте функцию, которая возвращает объект, к которому прикреплен ключ name
к foo
». Если я это сделаю providerMap.foo('bar')
, то я верну функцию, которая присоединяет name
к bar
, но эта функция была связана с foo
на карте — это ошибка! Еще я хочу, чтобы эта карта точно сообщала мне сигнатуру функции для каждой клавиши. Итак, там сказано: ключ foo
сопоставляется с функцией, которая принимает строку. bar
в функцию, которая принимает число и т. д. Ключ карты и возвращаемое значение ключа name
объекта должны совпадать.
@jcalz , чтобы добавить : обратите внимание, что на игровой площадке TS вы поделились картами providerMap
с фабриками, такими как foo: providerFuncFromStringFactory
, но в моем исходном фрагменте кода из моего вопроса я хочу сопоставить частично примененные фабрики, например foo: providerFuncFromStringFactory('foo')
.
@jcalz извини, ты абсолютно прав. И ваше решение отвечает всем моим требованиям, спасибо! Почему-то я не заметил, чтобы вы использовали mappy(
в этой строке: const providerMap = mappy({
. Из-за этого я думал, что если я сделаю console.info(providerMap.foo('bar'))
, то получу ProviderFuncFromString
с Name
, связанным с bar
, но на самом деле я получаю {"name": "foo", "data": "bar"}
, что является желаемым поведением. Еще раз спасибо за ответ и терпение!
Ладно, спасибо, я думал, что схожу с ума. Итак, резервное копирование... это всего лишь реализация mappy
, которая не проверена компилятором как безопасная (из-за as
внутри нее), но у пользователей mappy
такой проблемы нет. А реализация mappy
заботится о дедупликации, поскольку она использует ключ входного объекта как ключ и аргумент. В любом случае, я напишу ответ, когда у меня будет возможность.
Поскольку у вас есть ответ, который мне нравится больше, чем мой, я не буду его писать. Ключевое слово extends
по сути означает «x является частью y». В случае литеральных значений, таких как «foo», единственным значением x может быть «foo».
Чтобы полностью прекратить дублирование ключей, вам нужно сделать что-то вроде
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
там.
если я правильно понимаю суть, вы хотите, чтобы тип возвращаемого значения функции выводился и распространялся на дальнейший код, верно? если это так, я бы посмотрел что-нибудь вроде библиотеки Zod (github.com/colinhacks/zod). Определить схему; используйте его для анализа возвращаемого значения
return mySchema.parse(val)
; таким образом lsp TS сможет определить тип возвращаемого значения, поскольку он сообщит TS, что возвращаемое значение имеет тип, определенный схемой, или выдаст ошибку. Еще одним преимуществом является то, что схемы находятся во время выполнения, поэтому вы также обнаружите ошибки времени выполнения, что делает TS почти безопасным для типов.