Home > Mobile >  Angular template strict mode throws error when trying to pass a number to @Input x: string | number
Angular template strict mode throws error when trying to pass a number to @Input x: string | number

Time:10-29

I want to update my application to use Angular's template Strict mode by setting "strictTemplates": true inside tsconfig.json. After running ng serve with the new config, I got a weird error.

I have a shared component that is used in a lot of places

@Component({
  selector: 'my-shared-component',
  ...
})
export class SharedComponent {
  @Input() selectedId: string | number;
  @Output() selectedIdChange = new EventEmitter<string | number>()
  @Input() disabled: boolean;
  ...
}

and passing <my-shared-component disabled="true"> throws an error, which is fine.

The problem comes into play when I try to pass a number into the @Input() selectedId: string | number;, i.e.:

export class OtherComponent {
  myNumberId: number;
  ...
}

<my-shared-component [(selectedId)]="myNumberId">

And an error is thrown:

TS2322: Type 'string | number' is not assignable to type 'number'

I don't want to replace the type of myNumberId with string | number because it's also used in another component that has an @Input id: number (and must be a number). Also, myNumberId comes from the server and I know that is a number, so I think it will lead to poor design if I replace it with string | number.

I also don't want to use the strictAttributeTypes config to false because afterwards <my-shared-component disabled="true"> will no longer throw an error.

I was wondering if there exists some typescript utility type (or something similar) to make @Input() selectedId: string | number accept only strings or numbers, without forcing the variables from the parent to be string | number. Or can I do something else to fix the problem?

CodePudding user response:

Well, it rightly throws an error - the output EventEmitter will emit either string or number, which will try to assign a value to your strictly typed number.

What you could do is try separating a setter and getter in your OtherComponent:

_myNumberId: number;

get myNumberId(): number {
    return this._myNumberId;
}

get myNumberId(value: string | number): number {
    if (typeof value === 'string') {
      // This is a stub, you might want to go with something else like parseFloat()
      // or just throw.
      this._myNumberId = parseInt(value, 10);
    } else {
      this._myNumberId = value;
    }
}

This partially persists the number for the check types were values is fetched and still allows for setting the value from either string or number, so it will remove the compilation error. But it's not a good solution since it looks like a code smell, and it "loosens" the type checking on the setter.

If you take a step back and look at the bigger picture - perhaps you should define what an "Id" is in your application, and use that type in a generic component? Something along the way of:

export type MyId = string | number; 

Then, your component could use generic extending that type, which will do the strict type checking:

@Component({
  selector: 'my-shared-component',
  ...
})
export class SharedComponent<T extends MyId> {
  @Input() selectedId: T;
  @Output() selectedIdChange = new EventEmitter<T>()
  @Input() disabled: boolean;
  ...
}

This will allow for assigning either string or number as Input / Output in two-way binding and coercing the type to a single one of those in both places. And later on, if you decide you need to apply an additional logic to the Id itself, you can change it in one place (type definition) and see if it violates any type checks.

Of course you can always NOT define the type and do it like this:

export class SharedComponent<T extends string | number> {

but it seems you might want to enforce this in a few different places as well, so a custom value type seems like a good way to go.

Here is a stackblitz using the generics approach that correctly assigns both string and number values in two-way binding, but fails to assign a different type (Date). If you remove the date assignment, it will compile correctly.

CodePudding user response:

You can try replacing string | number with string & number.

And you can also try sending it like this :

<my-shared-component [(selectedId)]=" myNumberId">
  • Related