@soofftt91

Как описать OR логику в интерфейсах?

Нужно создать интерфейс в котором будут взаимоисключающие поля, а также набор общих/основных полей.

Пока пришел к таким решениям:
interface DefaultProps3 {
    instantPopoup?: boolean;
    // ...
}
interface PropsWithCb3 extends DefaultProps3 {
    path?: never;
    cb(): void;
}
interface PropsWithPath3 extends DefaultProps3 {
    cb?: never;
    path: string;
}
const test3: PropsWithCb3 | PropsWithPath3 = { // ERROR
    cb: () => {},
    instantPopoup: true,
    path: "df"
}

// =====================================

interface DefaultProps4 {
    instantPopoup?: boolean;
    // ...
}
interface PropsWithCb4 extends DefaultProps4 {
    action: "cb";
    cb(): void;
}
interface PropsWithPath4 extends DefaultProps4 {
    action: "path";
    path: string;
}
const test4: PropsWithCb4 | PropsWithPath4 = {
    action: "cb",
    cb: () => {},
    instantPopoup: true,
    path: "df", // ERROR - более явно указывает на ошибку
}

Второй вариант более явно указывает на место ошибки, но требует указывать дополнительный параметр.

Подскажите возможно есть устоявшиеся практики для решения подобных задач или способы написать подобную логику красивее/удобней?
  • Вопрос задан
  • 122 просмотра
Решения вопроса 1
@soofftt91 Автор вопроса
// utils.d.ts
type Without<T, U> = { [P in Exclude<keyof T, keyof U>]?: never };
type XOR<T, U> = (Without<T, U> & U) | (Without<U, T> & T);

// component.tsx
type DefaultProps = {
    instantPopoup?: boolean;
    // ...
}
type PropsWithCb = { cb(): void; }
type PropsWithPath = { path: string; }

type Props = DefaultProps & XOR<PropsWithCb, PropsWithPath>;
const fn = (props: Props): void => {}

const p1: Props = { instantPopoup: true, path: "/test" }; // good
const p2: Props = { instantPopoup: false, cb: () => {} }; // good
fn({ instantPopoup: false, path: "/test" }); // good
fn({ instantPopoup: false, cb: () => {} }); // good

const error1: Props = { instantPopoup: true, path: "/test", cb: () => {} }; // error
const error2: Props = { instantPopoup: true }; // error
fn({ instantPopoup: false, cb: () => {}, path: "" }); // error
fn({ instantPopoup: false }); // error

Еще как вариант можно использовать это: https://github.com/maninak/ts-xor
Ответ написан
Комментировать
Пригласить эксперта
Ответы на вопрос 1
Aetae
@Aetae Куратор тега TypeScript
Тлен
Увы конкретный тип может так работать только при наличии дискриминатора как в варианте 2 или жутких костылей как в варианте 1.

Однако, если вам это нужно для параметров некоей функции(или react компонента), то в случае передачи аргументом литерала оно может красиво работать с помощью пергрузок:
interface DefaultProps4 {
    instantPopoup?: boolean;
    // ...
}
interface PropsWithCb4 extends DefaultProps4 {
    cb(): void;
}
interface PropsWithPath4 extends DefaultProps4 {
    path: string;
}


type Magic = ((arg: PropsWithCb4) => 1) & ((arg: PropsWithPath4) => 2);
declare const magic: Magic;

// error
magic({ 
    cb: () => {},
    instantPopoup: true,
    path: "df", 
});

// 1, no error
magic({ 
    cb: () => {},
    instantPopoup: true,
})

// 2, no error
magic({ 
    instantPopoup: true,
    path: "df", 
})


Но только в случае литерала(установка пропсов в jsx - тоже считается литералом), вот так уже не сработает:
const trololo = {
    cb: () => {},
    instantPopoup: true,
    path: "df",
}
magic(trololo); // 1, no error
Ответ написан
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Войти через центр авторизации
Похожие вопросы