Типизируйте объект опций, где два поля должны быть либо оба, либо ни одного
Напишите обобщённый помощник AllOrNothing<T>, принимающий либо все ключи T, либо ни одного.
Требования: передача всех ключей должна проходить проверку; передача пустого объекта должна проходить проверку; передача строгого непустого подмножества должна быть ошибкой компиляции. Не добавляйте проверку во время выполнения и не перечисляйте ключи руками — выведите их из T.
type AllOrNothing<T> = /* ваш код здесь */;
type Range = AllOrNothing<{ start: Date; end: Date }>;
const a: Range = { start: new Date(), end: new Date() }; // ок
const b: Range = {}; // ок
const c: Range = { start: new Date() }; // должна быть ошибка
Допишите реализацию.
Объедините полную форму с формой, все ключи которой необязательны и типа never: type AllOrNothing<T> = T | { [K in keyof T]?: never }. Вторая половина — mapped-тип поверх keyof T, поэтому она следует за T сама. Передача только start не подходит ни к одной ветке — в первой нет end, а во второй значение получает never, — и компилятор её отвергает.
- ✗Считать, что два необязательных поля уже ограничивают друг друга — это не так
- ✗Пересекать половины вместо объединения, получая непригодный тип
- ✗Сдаваться и уносить инвариант в проверку во время выполнения
- →Как расширить это до исключающего «или» между двумя разными формами?
- →Почему проверка лишних свойств важна здесь для ветки с пустым объектом?
Инвариант «оба поля или ни одного» выражается объединением двух форм: полной и «пустой», у которой все ключи необязательны и имеют тип never. Значение типа never создать нельзя, поэтому вторая ветка принимает только объект без этих ключей.
type AllOrNothing<T> = T | { [K in keyof T]?: never };
type Range = AllOrNothing<{ start: Date; end: Date }>;
const a: Range = { start: new Date(), end: new Date() }; // ок — ветка T
const b: Range = {}; // ок — ветка never
// @ts-expect-error — нет end, а start не может быть never
const c: Range = { start: new Date() };
Ключи второй ветки берутся из keyof T, поэтому при добавлении поля в исходную форму ограничение расширяется само — руками ничего дописывать не нужно.