Type checking for SimpleChanges interface in ngOnChanges hook

7.9k views Asked by At

It would be great if we had type checking inside the SimpleChanges typescript parameter of ngOnChanges hook for the current Component.

This would prevent us from errors in the properties we check.

4

There are 4 answers

1
George Mavritsakis On BEST ANSWER

Using TypeScript 2.1 and the keyof functionality I have figured out the following type declarations (based on SimpleChanges) which seems to give us the necessary typed access to Component's properties:

export type ComponentChange<T, P extends keyof T> = {
    previousValue: T[P];
    currentValue: T[P];
    firstChange: boolean;
};

export type ComponentChanges<T> = {
    [P in keyof T]?: ComponentChange<T, P>;
};

Using those, declarations vscode editor automatically fetches the type info and auto completes the changes properties:

enter image description here

One problem though is that the changes parameter now will list each and every property of the component (and not only the @Input() properties) but I haven't figure out a better way than this.

0
Ashg On

I solve the issue using inheritance. First I create typed interface which extends SimpleChange class. You only need to do this once and import it in your components which need it.

interface TypedChange<T> extends SimpleChange
{
    previousValue: T;
    currentValue: T;
}

Second we extend the SimpleChanges interface with ones expected to change in our component. For example

interface MyComponentChanges extends SimpleChanges
{
    RefreshQueue: TypedChange<number>
}

Lastly the implementation is

public ngOnChanges(changes: MyComponentChanges): void
{
    if (changes.RefreshQueue)
    {
        if (!changes.RefreshQueue.isFirstChange())
        {
            if (changes.RefreshQueue.currentValue == 1)
            {
                DoSomething();
            }
        }
    }
}

Intellisense screen shot below

enter image description here

1
homk On

There is a solution from Netanel Basal

type MarkFunctionPropertyNames<T> = {
  [Key in keyof T]: T[Key] extends Function | Subject<any> ? never : Key;
}
type ExcludeFunctionPropertyNames<T extends object> = MarkFunctionPropertyNames<T>[keyof T];
type ExcludeFunctions<T extends object> = Pick<T, ExcludeFunctionPropertyNames<T>>;
export type NgChanges<TComponent extends object, TProps = ExcludeFunctions<TComponent>> = {
  [Key in keyof TProps]: {
    previousValue: TProps[Key];
    currentValue: TProps[Key];
    firstChange: boolean;
    isFirstChange(): boolean;
  }
}
// ...
@Component({...})
export class MyComponent {
  ngOnChanges(changes: NgChanges<MyComponent>) { }
}
0
mmjr-x On

I suggest the following solution, adapted from https://github.com/angular/angular/issues/17560#issuecomment-624161007

export type SimpleChangeTyped<T> = Omit<SimpleChange, SimpleChange['previousValue'] | SimpleChange['currentValue']>
  & {
    previousValue: T;
    currentValue: T;
  };

export type SimpleChangesTyped<T> = {
  [K in keyof T]: SimpleChangeTyped<T[K]>;
};

This solution has the following advantages:

  • Fully typed / Works with intellisense
  • Does not overwrite/union's properties that inherently present on SimpleChange
  • Works with this
  • Protects against bad properties
  • Will throw an error if the angular devs ever decide to change the previousValue or currentValue properties on SimpleChange

UsageExample:

ngOnChanges(changes: SimpleChangesTyped<YourComponent>): void { // Or use SimpleChangesTyped<this>
    changes.someComponentProperty; // GOOD: Will identify the changes object with correct types
    changes.someOtherProperty; // BAD: Property 'someOtherProperty' does not exist on type 'SimpleChangesTyped<YourComponent>'.ts(2339)
    changes.someComponentProperty.currentValue; // GOOD: Will identify the type of the someComponentProperty property
    changes.someComponentProperty.firstChange; // GOOD: Will identify the type of the SimpleChange firstChange property (boolean)
    changes.someComponentProperty.someOtherProperty; // BAD: Property 'someOtherProperty' does not exist on type 'SimpleChangeTyped<SomeComponentPropertyType>'.ts(2339)
  }

The only downside of this method is that it does not inherit/union of SimpleChanges and thus if any properties are added to this interface in the future they will not be valid for SimpleChangesTyped. I tried using the following variant:

export type SimpleChangesTyped<T> =
  Omit<SimpleChanges, keyof T> &
  {
    [K in keyof T]: SimpleChangeTyped<T[K]>;
  };

However sadly this does not work, since you end up allowing all key's again.