restricting the type on function arguments in nodejs and typescript

You’re not at all alone being surprised by this. 🙂 One of the key things about the TypeScript type system is that it’s structural (based on structure), not nominal (based on names). As long as something has the minimum structure necessary, it matches even if it has a different ancestry. That means any object will be accepted by the type system as your DTO type because your DTO type has no properties, so all objects match it.

That’s mostly a feature, but sometimes you want to disable it. The usual approach when you want to disable it is to use a branding property:

class DTO {
    __brand = "DTO" as const;
}

Now, only objects that have a __brand property with the value "DTO" will be allowed where DTO objects are expected by the type system.


Here’s a complete example with some minor changes to be more in keeping with JavaScript/TypeScript naming conventions and to supply some bits that were missing in the question code (presumably to keep it short! 🙂 ):

class DTO {
    __brand = "DTO" as const;
}

class UserDTO extends DTO {
    /* Commenting these out as they're not relevant to the question.
    @IsDefined({message:"Username required"})
    @Expose()
    @Length(1,10, {message:"min 1 max 10"})
    */
    username: string;
    
    constructor(username: string) {
        super();
        this.username = username;
    }
}
  
class BadDTO {
    name: string = "";
}

function validateDTO(objToValidate: DTO, validateMissingProperties: boolean): string[] {
    return [];
}

// Okay
validateDTO(new UserDTO("Joe"), true);

// Disallowed by the type system
validateDTO(new BadDTO(), false);

Playground link


Side note 2: In that example I added a constructor to UserDTO that initialized the username property. TypeScript has a shorthand for when you want to use a constructor paramter to initialize an instance property, this is functionally identical to the UserDTO in my example:

class UserDTO extends DTO {
    /* Commenting these out as they're not relevant to the question.
    @IsDefined({message:"Username required"})
    @Expose()
    @Length(1,10, {message:"min 1 max 10"})
    */
    //−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−− note no `username` declaration here
    
    constructor(public username: string) {
    //          ^−−−−−−−−−−−−−−−−−−−−−−−−−−−−−− note adding `public`
        super();
        // −−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−− note no code here to do the
        // initialization; it's implicit in the `public` declaration above
    }
}

Which you use is a matter of style.

CLICK HERE to find out more related problems solutions.

Leave a Comment

Your email address will not be published.

Scroll to Top