It's not really possible to do this in a way that the TypeScript compiler will verify as type-safe. The easiest thing you can do, by far, is to use a type assertion to just tell the compiler that you're doing the right thing:
const convertNonGenericManual = (input: Credentials): XmlCredentials => {
return Object.fromEntries(
Object.entries(input).map(([key, value]) => [key, { _text: value }])
) as XmlCredentials; // okay, no compiler error
};
That as XmlCredentials
at the end is you asserting that the output of Object.fromEntries()
will be a valid XmlCredentials
. The compiler believes you and lets you move on without complaint. This shifts responsibility for verifying type safety away from the compiler (which can't figure it out) and onto you (who hopefully can). That means you should double and triple check your code for errors before using an assertion. If you make a mistake, the compiler might not catch it:
const convertNonGenericManualOops = (input: Credentials): XmlCredentials => {
return Object.fromEntries(
Object.entries(input).map(([key, value]) => ["key", { _text: value }])
) as XmlCredentials; // still okay, not compiler error
};
Oops, this produces an object with one key named "key"
instead of a valid XmlCredentials
. So be careful.
Why can't the compiler verify type safety in your implementation?
The primary reason: the TypeScript typings for the Object.entries()
method and the similar Object.keys()
method are less specific than you might hope for. Object types in TypeScript are extendible and not sealed. You could have a value of type Credentials
that has more than just the Username
and Password
properties. This is valid TypeScript:
interface CredentialsWithCheese extends Credentials {
cheese: true
}
const credentialsWithCheese: CredentialsWithCheese = {
Username: "ghi",
Password: "jkl",
cheese: true
}
const alsoCredentials: Credentials = credentialsWithCheese;
Every CredentialsWithCheese
is also a Credentials
. So TypeScript only knows that Object.entries(input)
produces key-value tuples with keys of type string
, since it cannot restrict the keys to just "Username"
or "Password"
. There might be "cheese"
or anything else in there. So the resulting object will only be known to have string
keys too. Which is too wide for XmlCredentials
.
See this SO question for an authoritative description of this issue.
So you'd be better off using an array of only those keys you care about, like ["Username", "Password"] as const
, using a const
assertion to tell the compiler to keep track of the literal types of those keys (and not just string[]
).
That still doesn't fix things, though; the language has no way to track that the result of the output of the map()
method or the behavior of the forEach()
method is exhaustive of the XmlCredentials
properties. The best you can do is get the compiler to see that you have a Partial<XmlCredentials>
(using the Partial<T>
utility type to be a version of T
with all optional properties):
const convertNonGenericManual = (input: Credentials): XmlCredentials => {
const ret: Partial<XmlCredentials> = {};
(["Username", "Password"] as const).forEach(k => ret[k] = { _text: input[k] });
return ret as XmlCredentials;
}
And so at the end, you still have to assert that your Partial<XmlCredentials>
is an actual XmlCredentials
. See this SO question for more information about the inability of the compiler to verify that an iterative process to build an object is exhaustive.
So the above refactoring is a little more type safe, but still you are ultimately asserting things the compiler can't see. It's up to you whether the refactoring is worth it or not.
Playground link to code