Общий вывод Typescript из реализации интерфейса

Я пытаюсь вывести тип возвращаемого значения метода из универсального переданного аргумента. Однако аргумент является реализацией из универсального интерфейса, поэтому я предполагаю, что вывод машинописного текста определил бы тип из базы аргумента.

Пример кода:

interface ICommand<T> {}

class GetSomethingByIdCommand implements ICommand<string> {
  constructor(public readonly id: string) {}
}

class CommandBus implements ICommandBus {
  execute<T>(command: ICommand<T>): T {
    return null as any // ignore this, this will be called through an interface eitherway
  }
}

const bus = new CommandBus()
// badResult is {}
let badResult = bus.execute(new GetSomethingByIdCommand('1'))

// goodResult is string
let goodResult = bus.execute<string>(new GetSomethingByIdCommand('1'))

Я бы хотел сделать первый execute вызов и сделать так, чтобы машинописный текст вынул правильное возвращаемое значение, которое в данном случае равно string в зависимости от того, из чего GetSomethingByIdCommand было реализовано.

Я пробовал играть с условными типами но не уверен, что это решение или как его применить.


person Shawn Mclean    schedule 21.03.2019    source источник
comment
Возможно, проблема здесь в том, что для вашего ICommand<T> интерфейса нет ограничений в отношении того, как T используется. Из-за этого ваш GetSomethingByIdCommand класс также неявно реализует ICommand<number> (например). Действительно, кажется, что класс является реализацией ICommand<T> для любого типа T. Учитывая это, как TypeScript выбирать, какой тип выводить?   -  person CRice    schedule 22.03.2019
comment
@CRice Я этого не совсем понимаю. Как GetSomethingByIdCommand неявно реализует ICommand<number>, если явно реализует ICommand<string>? Вы можете уточнить?   -  person Jake Holzinger    schedule 22.03.2019


Ответы (3)


Ваша проблема в том, что ICommand<T> структурно не зависит от T (как указано в комментарии @CRice).

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

Система типов TypeScript является (в основном) структурной, а не номинальной: два типа одинаковы тогда и только тогда, когда они имеют одинаковую форму (например, имеют одинаковые свойства), и это не имеет никакого отношения к тому, имеют ли они одинаковое имя. Если ICommand<T> не структурно зависит от T и ни одно из его свойств не имеет ничего общего с T, тогда ICommand<string> является тем же типом, что и ICommand<number>, то есть того же типа как ICommand<ICommand<boolean>>, который относится к тому же типу, что и ICommand<{}>. Да, это разные имена, но система типов не является номинальной, так что это не имеет большого значения.

В таких случаях нельзя полагаться на вывод типа. Когда вы вызываете execute(), компилятор пытается вывести тип для T в ICommand<T>, но ничего не может сделать. Таким образом, по умолчанию используется пустой тип {}.

Чтобы исправить это, нужно сделать ICommand<T> структурно зависимым от T и убедиться, что любой тип, реализующий ICommand<Something>, делает это правильно. Один из способов сделать это с учетом вашего примера кода:

interface ICommand<T> { 
  id: T;
}

Таким образом, ICommand<T> должен иметь свойство id типа T. К счастью, GetSomethingByIdCommand действительно имеет свойство id типа string, как того требует implements ICommand<string>, так что компилируется нормально.

И, что немаловажно, вывод, который вы хотите, действительно происходит:

// goodResult is inferred as string even without manually specifying T
let goodResult = bus.execute(new GetSomethingByIdCommand('1'))

Хорошо, надеюсь, что это поможет; удачи!

person jcalz    schedule 22.03.2019
comment
Отличный ответ. Это, конечно, не очень интуитивно понятно, если вы переходите с другого языка, который поддерживает дженерики, например, Java. - person Jake Holzinger; 22.03.2019
comment
Кроме того, если вы находитесь в ситуации, когда вы не можете полагаться на наличие значения для вашего свойства тега (в приведенном выше примере id), вы можете определить его как член и использовать оператор определенного присваивания, чтобы избежать его установки: id!: string; - person y2bd; 22.03.2019
comment
@ y2bd не могли бы вы объяснить это поподробнее? У меня были проблемы с его использованием в интерфейсе, поэтому я использовал id?: string, который также применялся к реализации класса. - person Shawn Mclean; 22.03.2019
comment
@ShawnMclean Вот пример: repl.it/repls/WoozyStupidAtoms - person y2bd; 22.03.2019

Кажется, что Typescript может правильно вывести тип, если конкретный тип приведен к его универсальному эквиваленту до того, как он будет передан в ICommandBus.execute():

let command: ICommand<string> = new GetSomethingByIdCommand('1')
let badResult = bus.execute(command)

Or:

let badResult = bus.execute(new GetSomethingByIdCommand('1') as ICommand<string>)

Это не совсем элегантное решение, но оно работает. Очевидно, что обобщенные типы машинописных текстов не очень функциональны.

person Jake Holzinger    schedule 21.03.2019

TS не может вывести интерфейс, который реализует этот метод, так, как вы этого хотите.

Здесь происходит следующее: когда вы создаете экземпляр нового класса:

new GetSomethingByIdCommand('1') 

Результатом создания экземпляра нового класса является объект. По этой причине execute<T> вернет объект вместо ожидаемой строки.

Вам нужно будет выполнить проверку типа после того, как функция execute вернет результат.

В случае объекта vs строка вы можете просто выполнить проверку типа.

const bus = new CommandBus()
const busResult = bus.execute(new GetSomethingByIdCommand('1'));
if(typeof busResult === 'string') { 
    ....
}

Это отлично работает во время выполнения, когда машинописный текст скомпилирован в простой JS.

В случае объектов или массивов (которые также являются объектами: D) вы должны использовать защиту типа.

Type Guard пытается применить к чему-либо элемент, проверяет, существует ли свойство, и делает вывод, какая модель использовалась.

interface A {
  id: string;
  name: string;
}

interface B {
  id: string;
  value: number;
}

function isA(item: A | B): item is A {
  return (<A>item).name ? true : false;
}
person Vlatko Vlahek    schedule 21.03.2019
comment
На самом деле вопрос заключается в том, как заставить вывод типов работать интуитивно понятным образом. Ожидается, что badResult будет иметь тип string, потому что execute() дан объект, который реализует ICommant<T>, а T привязан к _6 _... - person Igor Soloydenko; 22.03.2019
comment
К сожалению, в этом сценарии вывод TS немного ограничен. Вы не можете сделать вывод о реализованном интерфейсе таким образом. Результатом создания экземпляра нового класса всегда является объект, поэтому возвращаемый тип выводится как объект по этой причине. - person Vlatko Vlahek; 22.03.2019