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:
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)?
Is there a TS setting or technique I can employ to get TS to error in ††† above?
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With