Let's say I have this code:
export class ProductsListComponent {
#Output() onProductSelected: EventEmitter<Product>;
constructor() {
this.onProductSelected = new EventEmitter();
}
}
This is some example of EventEmitter usage. I don't understand why first we declare onProductSelect explicitly stating that it is EventEmitter that carries Product instance, and then we instantiate it with just new EventEmitter(). Why not new EventEmitter<Product>()?
I think that in C# I would have to go with the second way, because otherwise it wouldn't compile if EventEmitter was generic.
Why doesn't TypeScript require that?
//EDIT:
Further clarification fo my question.
What's the difference between:
#Output() onProductSelected: EventEmitter<Product>;
this.onProductSelected = new EventEmitter();
and
#Output() onProductSelected: EventEmitter;
this.onProductSelected = new EventEmitter();
As explained in documentation chapter, type argument inference occurs when type isn't specified for generic class or function.
A function can infer T type from its argument or return types, and a class can infer T type from constructor argument or return types:
function foo<T>(v: T) { return v }
foo(1); // T inferred to number
class Foo<T> {
constructor(v: T) {}
}
new Foo(1); // T inferred to number
If there's nothing to infer, T is inferred to empty object {} for some reason:
class Foo<T> {
foo(v: T) {}
}
new Foo().foo(1); // T inferred to {}
In order to avoid inference to {}, default type can be provided:
class Foo<T = string> {
foo(v: T) {}
}
new Foo().foo(1); // type error
If generic class or function isn't supposed to be used with default type, some impossible type can be specified:
class Foo<T = never> {
foo(v: T) {}
}
new Foo(); // no type error, T isn't involved
new Foo().foo(<any>1); // type error
Since EventEmitter generic class has no default type specified, the latter is inferred to {}. There usually won't be problems with that, because emit is the only method that is affected by generic type. Since all non-nully types can be coerced to object type, this usually won't cause type errors - as long as nully types are ignored.
strictNullChecks compiler option will be a problem for EventEmitter with default type and nully values:
const ee = new EventEmitter();
ee.emit(null); // type error
So for all-round EventEmitter it shouldn't rely on default type and be instantiated as:
const ee = new EventEmitter<any>();
EventEmitter() will work the same as EventEmitter<any>().
If you draw this in HTML on the selector of ProductsListComponents,
then you can listen to onProductSelected event and assign an action like onSelected when that happens. By default you'll get a new EventEmitter<any>(), and defining a variable of any type is the generic approach in Typescript.
<product-list (onProductSelected)="onSelected($event)"> </product-list>
So every time in your child you call
this.onProductSelected.emit("hello");
this.onProductSelected.emit(1);
Parent's function onSelected($event) will get called and you can do anything with that data.
If you're only expecting one type of data to get outputted to the parent so it can further handle it, then you'd want to stick to a specific data type.
onProductSelected: Product = new EventEmitter<Product>();
products: Product[];
then somewhere in your code you can trigger to emit
this.onProductSelected.emit(this.products[1]);
Added an example in stackblitz
Regarding your updated question
#Output() onProductSelected: EventEmitter; is an error, because here you're declaring a type (after :, before =), as oppose to defining a type (after =), when you declare it as an EventEmitter you do need an argument <type>
If you declare the type of EventEmitter, then the compiler will make sure you don't emit anything other than Product type, or what type you declare it to be.
#Output() onProductSelected: EventEmitter<Product>;
Related
I am attempting to author a class that will give me type safety for each key + value specified in an interface and passed as a generic.
I've managed to get everything working EXCEPT that I am unable to ensure that a key belongs to the interface. On other words: I can pass string values that ARE NOT found within the interface provided to a generic.
I just cannot figure out how to adjust my code so that I can guarantee the key exists in the interface.
An ultra-minimal version of the code:
type Listener<V = unknown[]> = (...values: V[]) => void;
type EventMap = Record<string, Listener>;
type Library<T> = Partial<Record<keyof T, Set<T[keyof T]>>>;
class EventBlaster<T extends EventMap> {
lib: Library<T> = {};
register<K extends keyof T>(eventName: K, listener: T[K]) {}
trigger<K extends keyof T>(eventName: K, ...values: Parameters<T[K]>) {}
}
///
/// Usage
type CustomMap = EventMap & {
change(value: string): void;
};
const customEvents = new EventBlaster<CustomMap>();
// Working register / trigger
customEvents.register('change', (value) => {});
customEvents.trigger('change', 'hello');
// TypeScript correctly throwing errors on wrong listener arguments
customEvents.trigger('change', '1st string', '2nd string');
// TypeScript failing to throw errors on undeclared event names
customEvents.register('nope', () => {});
customEvents.trigger('nope');
In that example, I've got all of the listener > argument values working as expected - which is great and I assumed that would be the hard part...
But unfortunately, you can see that I am able to call register / trigger with string values that are not keys on the CustomMap type.
Any help in resolving this would be greatly appreciated! It would also be great if I didn't have to extend EventMap every time I defined a interface to provide as the generic argument for EventBlaster... but my assumption is that this is a requirement to get the behaviour that I am after.
A slightly more robust example of the code can be found in this TypeScript Playground link.
I tried to create a base class that has a static utility method to construct the class, which uses return new this(). But it doesn't work when I have a subclass extending it, because that utility method returns the base class instead of subclass, which is not the same as JavaScript.
Minimal Example:
class Base {
static create() {
return new this()
}
}
class Sub extends Base {
fn() {
return this
}
}
Sub.create().fn() // TypeScript error, JavaScript ok
Base.create().fn() // TypeScript error, JavaScript error
TypeScript Playground
It's because TypeScript doesn't have polymorphic this types for static methods. There's a longstanding open issue, microsoft/TypeScript#5863, asking for such a feature. For now it's not part of the language, but luckily comments on that issue describe a workaround that should get you the behavior you're looking for:
class Base {
static create<T extends Base>(this: new () => T) {
return new this()
}
}
Here we are making create() a generic method with a parameter of type T extends Base that can only be called on an object whose this context is a zero-arg constructor that returns a value of type T.
Therefore the compiler knows that Sub.create() will return a Sub and that Base.create() will return a Base:
Sub.create().fn() // okay
Base.create().fn() // Property 'fn' does not exist on type 'Base'
This also has the advantage over your existing code in that the following is an error now:
class Oops extends Base {
x: string;
constructor(x: string) {
super();
this.x = x.toUpperCase();
}
}
Oops.create(); // error!
The Oops class does not have a zero-arg constructor, and so the implementation of Oops.create() will end up calling new Oops() without the required parameter and cause a runtime error when it tries to call toUpperCase() on undefined.
Okay, hope that helps; good luck!
Playground link to code
I have an abstract class that does new this(), however, it isn't creating an instance of itself, but it is creating an instance of the class that extended it.
This works in JavaScript when compiled and returns the proper class. However, TypeScript is complaining.
Cannot create an instance of an abstract class.
abstract class Model {
static find<T extends Model>(someVar) {
let inst = new this() as T
// Do some extra stuff with the instance
return inst
}
}
class A extends Model { }
A.find()
The first problem of your solution - it isn't safe, T type may not contain empty constructor. The second problem: even if you call find() on A class, return type will be inferred as Model if it isn't set explicitly.
Because anyway you should specify A type when you call find, it would be better to pass the type constructor to the find method, in this case both problems will be solved.
abstract class Model {
static find<T extends Model>(ModelClass: new () => T, param: string): T {
let inst = new ModelClass()
// Do some extra stuff with the instance
return inst;
}
}
class A extends Model {}
const a = Model.find(A, "param");
I'm just a poor programmer who inherited some TypeScript code. Sometimes you just want a clue what is going on without taking a few weeks off to become fluent in a new language. I have an interface:
interface IJQuery {
addRecordInline(object: any);
}
I'm getting an error on addRecordInline:
"Named property type '(object any) => any' must be assignable to string indexer type 'HTMLElement': Type 'HTMLElement' has non-optional property 'accessKey' which is not present in type '(object any) => any'
Just any kind of clue as to what is going on would be appreciated. I've looked around the internet ... there are some posts about Indexers that seem the closest. But what is happening here? Just a pointer to some information. Thank you.
EDIT:
Examples of the interface being implemented:
interface IDocumentManager {
UpdateForm: IJQuery;
UpdateActionUrl: string;
DocIdPrefix: string;
}
2nd EDIT:
Here is a class that implements the interface:
class MemberDocumentManager implements IDocumentManager {
private ConfirmDeleteButton: IJQuery;
// other declarations removed
constructor() {
this.ConfirmDeleteButton = $('#deleteConfirmButton');
}
}
A JQuery object is being assigned to a member that implememnts the interface in question, is that the problem?
Based on the error, that is not the only definition of IJQuery. As far as I can tell this is specific to your project and is not part of the JQuery library.
Form the error, there is probably a definition of IJQuery that contains an indexer:
interface IJQuery {
[name: string]: HTMLElement
}
Which means all properties defined on this type must be of type HTMLElement
interface IJQuery {
addRecordInline(object: any) : void; // Not ok
anElement: HTMLElement // OK
}
You can either relax the restriction by removing the indexer ([name: string]: HTMLElement) or define the method on another interface or change the indexer to return either an element or a function ([name: string]: HTMLElement | Function)(although this will probably break your code in several places)
I am trying to observe the latest event that happens in a stream. Each event can be one of two types, represented by an enum. However I am getting a compilation error in the following example:
import { BehaviorSubject } from 'rxjs/Rx';
export enum FilterAction {
RESET, UPDATE
}
export class FilterSomething {
private _filterEvent: BehaviorSubject<FilterAction> = new BehaviorSubject(FilterAction.RESET);
get filterEvent() {
return this._filterEvent.asObservable(); // <-- behave.ts(14,12): error TS2322: Type 'Observable<FilterAction>' is not assignable to type 'FilterAction'
}
set filterEvent(action: FilterAction){
this._filterEvent.next(action);
}
}
Compilation error:
behave.ts(14,12): error TS2322: Type 'Observable<FilterAction>' is not assignable to type 'FilterAction'
However when I use the generic any it compiles (and works).
set filterEvent(filterAction: any) {
this._filterEvent.next(filterAction);
}
I suspect it's something to do with the enum values, as opposed to instances of FilterAction.
The problem is return type of your getter. Getters and Setters should take and return same type of parameter, which is FilterAction in your case. But your getter returns Observable<FilterAction>. Just do not use getter, change the name of the function. Then language service stop showing error. In language specification:
If both accessors include type annotations, the specified types must be identical.
You can check this out.