Type checking unknown objects in typescript

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:

  1. 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.

  2. 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`.
     }