По сути, я пытаюсь добиться, чтобы функция возвращала правильный тип на основе ключа, переданного функции. Поэтому, если ключ service1
, правильный тип возврата должен быть Payloads['service1']
. Как я могу этого добиться?
interface Payloads {
service1: {
a: boolean;
};
service2: {
b: string;
};
service3: {
c: number;
};
};
const createPayload = <S extends keyof Payloads>(key: S): Payloads[S] => {
switch (key) {
case 'service1': return { a: true }
case 'service2': return { b: 'e' }
case 'service3': return { c: 3 }
default: throw new Error('undefined service')
}
}
Ошибка, которую я получаю:
В этой заметке, если вы ищете альтернативный подход, который поддерживает безопасность типов, вы можете провести рефакторинг для индексирования в объект, такой как это. Это соответствует вашим потребностям? Если это так, я могу написать ответ (при условии, что вопрос отредактирован, чтобы запросить такой ответ). Если нет, то что мне не хватает?
@jcalz, не могли бы вы предоставить ссылку, объясняющую, почему эта индексация объектов решает проблему, пожалуйста?
В последнем абзаце моего ответа есть эта информация (вы также можете прочитать на typescriptlang.org/docs/handbook/release-notes/…)
TypeScript не знает тип S
в теле функции, поэтому он ожидает, что вы передадите свойства все, чтобы убедиться, что Payloads[S]
выполняется. Но вы можете обмануть TypeScript! Если изменить тип возвращаемого значения на Payloads[keyof Payloads]
, это означает один из вариантов, и вы не получите никакой ошибки.
Теперь это изменило сигнатуру общедоступного метода, но мы этого не хотим. Чтобы это работало, мы должны использовать объявления function
, потому что они допускают перегрузки. Мы собираемся добавить перегрузку один к функции, которая является старой сигнатурой:
function createPayload<S extends keyof Payloads>(key: S): Payloads[S];
function createPayload<S extends keyof Payloads>(key: S): Payloads[keyof Payloads] {
// code here...
}
Это показывает Payloads[S]
вызывающей стороне, но внутри мы ожидаем Payloads[keyof Payloads]
. Полный рабочий пример здесь.
Но вы потеряли немного безопасности: case 'service2': return { c: 3 }
теперь принимается
Это верно, но я не знаю другого способа сделать это. Я думаю, что это просто невозможно с TypeScript.
Я согласен с @AndreaSimoneCosta, компилятор теперь позволяет возвращать объект со всеми объединенными свойствами, тогда как я хочу явно «указать» компилятору принимать только один тип из интерфейса Payloads
.
Тип Payloads[S]
— это индексированный тип доступа, который зависит от еще не определенного параметра типа универсальныйS
.
В вашей реализации компилятор не может использовать анализ потока управления для сужения параметра типа S
в операторе switch
/case
. Видит, что key
можно сузить, например, до "service1"
, но не сужает параметр типа S
. И поэтому он не знает, что { a: true }
будет присвоено Payloads[S]
в этом случае. Компилятор здесь слишком осторожен; единственное, что было бы приятно вернуть, — это значение, которое можно присвоить Payloads[S]
независимо от того, что такое S
, которое оказывается перекресток всех типов значений, эквивалентным {a: boolean; b: string; c: number}
. Поскольку вы никогда не возвращаете такое значение, компилятор жалуется.
В GitHub есть несколько открытых проблем, требующих некоторого улучшения. См., например, Майкрософт/TypeScript#33014. Однако на данный момент (начиная с TS4.6), если вы должны написать код, который работает таким образом, то компилятор не поможет вам проверить безопасность типов. Вам нужно будет взять на себя ответственность, используя что-то вроде утверждения типа
const createPayloadAssert = <S extends keyof Payloads>(key: S): Payloads[S] => {
switch (key) {
case 'service1': return { a: true } as Payloads[S]
case 'service2': return { b: 'e' } as Payloads[S]
case 'service3': return { c: 3 } as Payloads[S]
default: throw new Error('undefined service')
}
}
или один позывной перегрузка
function createPayloadOverload<S extends keyof Payloads>(key: S): Payloads[S];
function createPayloadOverload(key: keyof Payloads) {
switch (key) {
case 'service1': return { a: true };
case 'service2': return { b: 'e' };
case 'service3': return { c: 3 };
default: throw new Error('undefined service')
}
}
чтобы ослабить вещи достаточно, чтобы предотвратить ошибку. Это обязательно позволяет вам совершать ошибки, когда вы случайно переключаете возвращаемые значения. Но пока это лучшее, что вы можете сделать с switch
/case
.
Если вы хотите преобразовать свою реализацию в форму, в которой компилятор действительно может проверить безопасность вашего кода, вы можете сделать это, проиндексировав объект:
const createPayload = <S extends keyof Payloads>(key: S): Payloads[S] => ({
service1: { a: true },
service2: { b: 'e' },
service3: { c: 3 }
}[key]);
const createPayloadBad = <S extends keyof Payloads>(key: S): Payloads[S] => ({
service1: { a: true },
service2: { a: true }, // <-- error!
service3: { c: 3 }
}[key]);
Это работает, потому что, среди прочего, индексированные типы доступа были познакомился с TypeScript (называемые "типами поиска" в этой ссылке), чтобы представить на уровне типа то, что происходит, когда вы индексируете объект с ключом на ценностном уровне. То есть, если у вас есть объектно-подобное значение t
типа T
и ключеподобное значение k
типа K
, то при индексировании в t
с k
как t[k]
свойство, которое вы читаете, будет иметь тип T[K]
. Поэтому, если вы хотите, чтобы компилятор знал, что у вас есть значение типа Payloads[S]
, вы можете сделать это, проиндексировав значение типа Payloads
с ключом типа S
, как показано выше.
Обратите внимание, что ваш вопрос, как задано, "почему эта ошибка", а не "как ее исправить". Ответ на заданный вопрос таков: «Потому что компилятор не может сузить параметры универсального типа, такие как
S
, с помощью анализа потока управления, и поэтому компилятор ошибается в сторону консервативности в соответствии с мс/ТС#30769». Если вам также нужен способ сделать это с помощью рефакторинга для безопасности типов, вы можете захотеть редактировать вопрос, чтобы сказать об этом явно.