Как сделать свойство объекта необязательным на основе другого типа в TypeScript? - PullRequest
0 голосов
/ 29 января 2019

Я думаю, что лучший способ объяснить мой сценарий с помощью кода:

interface IPluginSpec {
  name: string;
  state?: any;
}

interface IPluginOpts<PluginSpec extends IPluginSpec> {
  name: PluginSpec['name'];
  // How to require opts.initialState ONLY when PluginSpec['state'] is defined?
  initialState: PluginSpec['state'];
}

function createPlugin<PluginSpec extends IPluginSpec>(
  opts: IPluginOpts<PluginSpec>,
) {
  console.log('create plugin', opts);
}

interface IPluginOne {
  name: 'pluginOne';
  // Ideally state would be omitted here, but I can also live with having to
  // define "state: undefined" in plugins without state
  // state: undefined;
}

// Error: Property 'initialState' is missing in type...
createPlugin<IPluginOne>({
  name: 'pluginOne',
  // How to make initialState NOT required?
  // initialState: undefined,
  // How to make any non-undefined initialState invalid?
  // initialState: 'anything works here',
});

interface IPluginTwo {
  name: 'pluginTwo';
  state: number;
}

createPlugin<IPluginTwo>({
  name: 'pluginTwo',
  initialState: 0,
});

1 Ответ

0 голосов
/ 29 января 2019

Вы можете сделать это с условным типом.С его помощью вы можете проверить наличие свойства и иметь или не иметь дополнительного свойства:

interface IPluginSpec {
  name: string;
  state?: any;
}

type IPluginOpts<PluginSpec extends IPluginSpec> = PluginSpec extends Record<'state', infer State> ? {
  name: PluginSpec['name'];
  initialState: State;
} : {
  name: PluginSpec['name']
}

function createPlugin<PluginSpec extends IPluginSpec>(
  opts: IPluginOpts<PluginSpec>,
) {
  console.log('create plugin', opts);
}

interface IPluginOne {
  name: 'pluginOne';
}

// Ok
createPlugin<IPluginOne>({
  name: 'pluginOne',
  // nothing to add
});

interface IPluginTwo {
  name: 'pluginTwo';
  state: number;
}

createPlugin<IPluginTwo>({
  name: 'pluginTwo',
  initialState: 0,
});

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

interface IPluginSpec {
    name: string;
    state?: any;
    config?: any;
}

type IPluginOpts<PluginSpec extends IPluginSpec> = {
        name: PluginSpec['name']
    }
    & (PluginSpec extends Record<'state', infer State> ? { initialState: State; } : {})
    & (PluginSpec extends Record<'config', infer Config> ? { initialConfig: Config; } : {})

Условный тип очень полезен для вызывающих.Проблема состоит в том, что внутри реализации typcript не может по-настоящему рассуждать об условных типах (поскольку T неизвестно).

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

function createPlugin<PluginSpec extends IPluginSpec>(opts: IPluginOpts<PluginSpec>)
function createPlugin<PluginSpec extends IPluginSpec>(opts: {
    name: string
    initalState: PluginSpec['state'],
    initialConfig: PluginSpec['config'],
}) {
    if (opts.initalState) {
        opts
    }
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...