1

Working on a Node.js project and using TypeScript.

I'm trying to restrict a functions argument type to a specific base class. I'm new with both Node & TypeScript and come from a C# background, so likely not quite understanding some of the characteristics of the lang.

Take these snippets.

First, my class declarations

class DTO{
}

class userDTO extends DTO{
    @IsDefined({message:"Username required"})
    @Expose()
    @Length(1,10, {message:"min 1 max 10"})
    username:String;    
  }
  
class badDTO {
    name:String;
}

Now I will create instances:

let user = new userDTO();
user.username = "My username";

let isUserDTO = user instanceof DTO; // Evaluates true

let bad = new badDTO();
bad.name = "Bob";

let isBadDTO = user instanceof DTO; // Evaluates false

Here is the signature of the method I intend to call

export default function ValidateDTO(objToValidate:DTO, validateMissingProperties:boolean): Array<string>{
    return [];
}

Finally, when I actually call the function.

let userErrors = ValidateDTO(user, true);

// Why is this allowed?
let badErr = ValidateDTO(bad, true);

I am expecting the 2nd ValidateDTO to show me a warning and not actually run because 'bad' is not a DTO as proven by instanceOf above - if i try passing a string as the 2nd arg I see an error, which is what i expected from passing a non-DTO as the first arg.

Can someone please show me where I am going wrong? How can I restrict the type of object passed into a function.

Happy to share other code as required too. Not sure what else i might be missing.

1 Answer 1

2

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.

Sign up to request clarification or add additional context in comments.

7 Comments

I love your explanation - thank you! When I try using your code example I get warnings in VS Code that "cannot find name 'as'" on the 'as' word and "';' expected" when on the const word. Tried copy/pasting too.
if i drop the 'as const' it works just as you described. Thanks so much - so is this the norm best practice?
@DarrenWainwright - I've updated the answer with a full example. You'll want the as const so that it's a compilation-time constant value, not just a string property whose value could be changed at runtime. :-) Yes, for the situations where you care about this, it's normal practice to use branding, but it's also normal practice to avoid caring where possible. :-D
T.K Crowder - thank you again. I can't get it to compile if i leave the 'as const' in place. just get the squiggles. I'm using Node 14.14 and TypeScript 4.0.5
ah, ok cool. THank you for the additional info, and side notes too. Super helpful
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.