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