У меня есть функция, которая получает аргумент options с атрибутом kind. Возможные значения для kind — это небольшой набор значений. Так что это в основном перечисление.

В зависимости от kind функция должна иметь другое возвращаемое значение. В то время как все возможные возвращаемые значения расширяются от некоторого общего базового типа.

Я могу добиться того, чего хочу, с помощью перегрузок, но тогда сама функция не очень хорошо типизирована:

function test(options: Scenarios['bar']['options']): Scenarios['bar']['ret'];
function test(options: Scenarios['foo']['options']): Scenarios['foo']['ret'];
function test(options: any): any {
  ...
};

Есть ли хороший способ ввести это с помощью дженериков? Было бы идеально, если бы if(options.kind === 'foo') { return ... } также корректно применял правильный тип возвращаемого значения.

Это то, что я пробовал, но это не работает.

type Base {
  a: string;
}

type Foo {
  b: string;
}

type Bar {
  c: string;
}

interface Scenarios {
  foo: { options: { kind: 'foo', input: string }, ret: Foo },
  bar: { options: { kind: 'bar' }, ret: Bar },
}

function test<S extends keyof Scenarios, O extends Scenarios[S]['options'], R extends Scenarios[S]['ret']>(options: O): R {
  const out: Partial<R> = {
    a: 'one',
  };
  if(options.kind === 'foo') {
    out.b = options.input;
  }
  if(options.kind === 'bar') {
    out.c = "whatever"
  }

  return out;
}

Здесь ни O, ни R, похоже, не набраны правильно. Я получаю несколько ошибок:

  • a: 'one', ошибки с литералом объекта могут указывать только известные свойства, а 'a' не существует в типе 'Partial'
  • options.input ошибок с Свойство "input" не существует для типа "O".
  • out.bout.c) ошибок с свойством 'b' не существует для типа 'Partial'.
4
Lux 22 Апр 2020 в 18:12

1 ответ

Я на самом деле не эксперт в этом вопросе, но, поиграв некоторое время с вашим запросом, я понял несколько вещей:

  1. TypeScript может различать только типы объединения; он не может различать дженерики.
  2. Указанная дискриминация не распространяется вверх; то есть TypeScript не будет автоматически различать родительский объект на основе распознавания дочерних объектов.

Как говорится, вот решение, которое я придумал:

type Base = { // I'm assuming this is the base type for Foo and Bar
    a: string;
}

interface Foo extends Base { // so I shall modify here a bit
    b: string;
}

interface Bar extends Base { // and here of course
    c: string;
}

interface Scenarios {
    // Now I put the return type inside the option, to make the discrimination work
    // Of course, I need to make 'ret' optional, or your input won't make sense
    foo: { kind: 'foo', input: string, ret?: Foo },
    bar: { kind: 'bar', ret?: Bar },
}

// Notice the use of 'Required' here; that brings the actual type of 'ret' back
function test<X extends keyof Scenarios>(options: Scenarios[X]): Required<Scenarios[X]>['ret'] {
    let data = options as Scenarios[keyof Scenarios]; // create a union type
    data.ret = { a: 'one' } as Scenarios[keyof Scenarios]['ret']; // type assertion

    if (data.kind === 'foo') {
        data.ret!.b = data.input; // finally the discrimination works!
    }
    if (data.kind === 'bar') {
        data.ret!.c = "whatever"
    }
    return data.ret!;
}

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

var result = test({ kind: 'foo', input: "aas" }); // oops

Тогда TypeScript все еще не может понять, что result имеет тип Foo. Но, конечно, такого рода автоматический вывод имеет очень низкую практическую ценность, поскольку даже если он и работает, он работает только тогда, когда вы буквально вводите слово «foo» в аргументе вашей функции, и если вы можете это сделать, что мешает вам ввести общий аргумент?

You may try these code in this Ссылка на игровую площадку

Обновить:

Я только что нашел решение моей последней проблемы:

function test<X extends keyof Scenarios>(options: { kind: X } & Scenarios[X]): Required<Scenarios[X]>['ret'] {
    ....
}

var result = test({ kind: 'foo', input: "aas" }); // It works!

Хитрость заключается в том, чтобы добавить { kind: X } к объявлению параметра.

See this Ссылка на игровую площадку.

1
Mu-Tsun Tsai 20 Июн 2020 в 06:44