Explanation of Problem
Imagine a reusable angular component which emits an unknown value. I recently came across this in an app which has a reusable modal for confirming an action. It takes in an unknown object which it also outputs when the action is confirmed. This is the output it contains:
@Output() actionConfirmed = new EventEmitter<unknown>();
This is typed correctly, but it does create a problem when using it because the method which should run when the EventEmitter
emits a value is strongly typed with the expected input type. The basic implementation looks like this:
<confirm-action-modal
(actionConfirmed)="doSomething($event)"></confirm-action-modal>
doSomething(input: MyDto): void {
// Implementation here
}
This gives the problem that $event
is unknown
, so isn't assignable to MyDto
.
Solutions
There are 3 solutions we can use, all of which involve the doSomething
method taking in an unknown
parameter and using a type check or assertion. The best approach depends on how the input
object was created.
When input was created via a constructor
If the input
was created with a typescript constructor (e.g. const dto = new MyDto();
) then we can use instanceof
to check the type.
doSomething(input: unknown): void {
if (!(input instanceof MyDto)) {
throw new Error('Input must be of type MyDto.');
}
// Implementation here. At this point, `input` is typed as `MyDto`.
}
When input was created without a constructor
Sometimes the input
will not have been created by calling new MyDto()
- for example, if it has come from JSON deserialisation. In these cases, instanceof
will always return false (because it checks the prototype chain). In this case, we should use one of the following approaches:
Write a type predicate to check the type. This is the safest option but also requires more work.
doSomething(input: unknown): void { if (!MyDto.isOfType(input)) { throw new Error('Input must be of type MyDto.'); } // Implementation here. At this point, `input` is typed as `MyDto`. }
The type predicate itself can be implemented anywhere, but I have used a static method on the DTO class:
static isOfType(input: unknown): input is MyDto { const typedInput = input as MyDto; return ( typeof typedInput.prop1 === 'string' && typeof typedInput.prop2 === 'number' && typeof typedInput.prop3 === 'number' ); }
Each property of the
MyDto
class needs checking appropriately, so if the class is very complex then this could become a big job.If you do not need to check the type, or it is an unreasonable amount of work to do so, we can simply assert it. This effectively suppresses warnings by describing the intended usage of the code. If the wrong type is passed into
doSomething
in the future, it will give unhandled errors.doSomething(input: unknown): void { const myDto = input as MyDto; // Implementation here. At this point, `myDto` is typed as `MyDto`, // but we haven't guaranteed that it actually is a `MyDto`. }