это не одно и то же
указывая через :
вы говорите, какого типа переменная или что возвращает метод
например
name: string
age: number
getAge() : namber {}
-------------------------------------------
Дженерик устроен сложнее и для другого
В вашем примере, переменная ingredientAdded имеет тип EventEmitter
т.е. справедливо
ingredientAdded : EventEmitter
но
сам EventEmitter должен содержать в себе какой-то тип данных (здесь - Ingredient)
По сути, EventEmitter может принимать примитивы или объекты и его работа будет неизменна
-------------------------------------------
Дженерики приятны, когда вы хотите определить какую-то логику, не завязываясь на тип данных. Что это значит?
Предположим, я хочу метод, который будет принимать на вход что угодно (строка, число, объект и т.д.), а возвращать массив этого чего угодно
function convertToArray<T>(arg: T): T[] {
let arr: T[];
//здесь логика заполнения этого массива
return arr;
}
теперь я могу вызывать
let numbers = convertToArray<number>(5)
let strings = convertToArray<string>('test')
let users = convertToArray<User>(new User())
Теперь у меня есть один метод, который делает одно и то же, но с разными типами данных
-------------------------------------------
Возвращаясь к EventEmitter. Эмиттер создает событие и держит внутри какие-то данные заданного типа
если вы делаете
@Output() ingredientAdded = new EventEmitter<Ingredient>();
, то теперь вы можете вызвать
ingredientAdded.emit(myIngredient)
, передав в emit экземпляр именно класса Ingredient, а не boolean, например
-------------------------------------------
А вообще, тема дженериков достаточно распространенная (Java, C#, TypeScript...), советую уделить ей внимание