Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Typescript: prevent assignment of object with more properties than is specified in target interface

Tags:

typescript

Say I have a typical 'user' object with the usual username, email, password, etc. properties. I want to create and manage an object that is a bona fide 'subset' of such a user with assurances that the password is NOT included. Here's a rough approach:

interface IUserSansPassword {
    username: string;
    email: string;
}

class UserSansPassword implements IUserSansPassword { ... }

interface IUser extends IUserSansPassword {
    password: string;
}

class User implements IUser { ... }

In trying to create an object of type IUserSansPassword, I expected the following to error:

const userSansPassword: UserSansPassword = new User(); // No TS Error †††

However, I don't get a TS error because, to my surprise, TS doesn't prohibit assigning objects with already established 'extra' properties. This is surprising since I would get an error if I tried to define directly with the extra property like so:

const userSansPassword: IUserSansPassword = {
    username: 'jake',
    email: '[email protected]',
    password: '' // TS Error ***
}

My questions in summary:

  1. Why does TS behave this way? Isn't it bad to allow assignment to a type with excess properties (hence why you get an error in *** above)?

  2. Is there a TS setting or technique I can employ to get TS to error in ††† above?

like image 682
Magnus Avatar asked Aug 14 '19 15:08

Magnus


Video Answer


1 Answers

The other answers here are essentially correct: types in TypeScript are generally open/extendable and can always have properties added; that is, they are not exact types (as requested in microsoft/TypeScript#12936) in which only known properties are allowed to exist. TypeScript doesn't really support exact types in general, although it does treat the types of freshly created object literals as exact types via excess property checks, as you've noticed.

If you really want to forbid a particular property key from a type in TypeScript, you can do this by making the property optional and have its type be never or undefined:

interface IUserSansPassword {
  username: string;
  email: string;
  password?: never; // cannot have a password
}

declare class UserSansPassword implements IUserSansPassword {
  username: string;
  email: string;
  password?: never; // need to declare this also
}

Now UserSansPassword is known not to have a defined password property. Of course now the following is an error:

interface IUser extends IUserSansPassword { // error! 
// Types of property "password" are incompatible
  password: string;
}

You can't extend IUserSansPassword by adding a password... if A extends B then you can always use an A instance where a B instance is expected. What you can do is extend a related type, your original IUserSansPassword, which can be computed using the Omit helper type:

interface IUser extends Omit<IUserSansPassword, "password"> {
  password: string;
}

declare class User implements IUser {
  username: string;
  email: string;
  password: string;
}

And then the following is an error like you expect:

const userSansPassword: UserSansPassword = new User();
// error, mismatch on "password" prop

Okay, hope that helps; good luck!

Link to code

like image 129
jcalz Avatar answered Oct 10 '22 01:10

jcalz