Typescript: only name/destructure optional properties when taking an interface parameter?

I'm new to typescript and am writing some classes to work as a very simple ORM for my project. I just learned how to require that a parameter conform to an interface, like so:

interface UserInfo {
    userId: string
    email?: string
    // many more properties...
}
class User {
    constructor(connection: typeof db, init: UserInfo) {
    // code here

And I understand that the syntax to set default values for those optional properties is:

class User {
    constructor(connection: typeof db, {email = null, /*...*/}: UserInfo) {
    // code here

But my UserInfo interface is long, what if I want to only specify/default a few of the properties, without typing out a giant object in the parameters for the constructor of this object? Is there a way to require an entire interface, but destructure only some properties?


Destructuring isn't really a great fit, because that essentially ignores any properties you don't explicitly mention. In order not to lose track of the unmentioned properties, you could destructure into a rest object:

class User {
  constructor({ email = "xyz", ...rest }: UserInfo) {}
}

But now you have a variable named email with a string value in it which is either the one passed in or the default "xyz" (not null, by the way; null is not a string) if none was passed in. And you also have a variable named rest with an Omit<UserInfo, "email"> value in it (I'm using the Omit<T, K> utility type to represent what happens when you widen a type to forget about some property keys).

If you actually need a value of type Required<UserInfo> (I'm using the Required<T> utility type to represent a type with any optional properties changed to be required), then you'll need to reassemble it from those pieces, such as via shorthand property names in your object literal along with object spreading:

class User {
  userInfo: Required<UserInfo>
  constructor({ email = "xyz", ...rest }: UserInfo) { // <-- split it apart       
    this.userInfo = { email, ...rest }; // <-- put it back together
  }
}

That works, but it is needlessly destructive (if you'll forgive the pun).


Instead, you can just use spreading to get more or less the same effect:

class User {
  userInfo: Required<UserInfo>
  constructor(init: UserInfo) {
    this.userInfo = { email: "xyz", ...init };
  }
}

If the passed-in constructor argument has no email property, then the default one of "xyz" will be there. Otherwise the argument's email property will overwrite the default.


Anyway in both cases you can see that it works as desired:

const u = new User({ userId: "abc" });
console.log(u.userInfo.userId) // abc
console.log(u.userInfo.email) // xyz

const v = new User({ userId: "def", email: "ghi" });
console.log(v.userInfo.userId) // def
console.log(v.userInfo.email) // ghi

Playground link to code