Issue
I'm learning Typescript from the excellent docs, and while practicing Mapped Types, I'm running into this error.
I know why this error is occuring (explained below) but since I'm new to TS, I don't know the recommended way of fixing it.
The code is below (and here's the link to the code playground):
// Represents schema of a Db table
type User = {
id: number; // might be auto incremented by DB
name: string; // contains textual name data
};
// Manual work. Define a type that represents schema of "User" and add some metadata
type DBFields = {
id: { format: "incrementing" };
name: { type: string; pii: true };
};
type ExtractPII<Type> = {
[Property in keyof Type]: Type[Property] extends { pii: true } ? true : false;
};
type ObjectsNeedingGDPRDeletion = ExtractPII<DBFields>;
// Manual work. Create object of ObjectsNeedingGDPRDeletion type
let deletionMarkers: ObjectsNeedingGDPRDeletion = {
id: false,
name: true
};
function handleGDPRDeletionRequest(user: User, deletionMarkers: ObjectsNeedingGDPRDeletion) {
let cleanedUser: Partial<User> = {};
for (const field of Object.keys(user)) {
if (!deletionMarkers[field as keyof User]) {
cleanedUser[field as keyof User] = user[field as keyof User];
}
}
console.log(cleanedUser); // Prints: { id: 1 }
}
let user = {
id: 1,
name: "John Doe",
};
handleGDPRDeletionRequest(user, deletionMarkers); // { id: 1 }
** Please correct me if this explanation is not accurate **
The error is happening at line
cleanedUser[field as keyof User] = user[field as keyof User];
because cleanedUser
is declared as a Partial<User>
. Partial<User>
means that all properties of User
are optional and can potentially be undefined
.
And when we try to assign user[field as keyof User]
, which can be a string
or number
to these potentially undefined
properties, TypeScript throws this error.
I tried asking ChatGPT and BingAI and they suggest some hacky solution like declaring cleanedUser
as any
which I don't want to do.
So I'm turning to SO for a proper solution.
Solution
TL;DR answer
Change the function to
function handleGDPRDeletionRequest(user: User, deletionMarkers: ObjectsNeedingGDPRDeletion) {
const cleanedUser: Partial<User> = Object.fromEntries(
Object.entries(user).filter(([userKey]) => !deletionMarkers[userKey as keyof User])
);
console.log(cleanedUser); // Prints: { id: 1 }
}
Why does it solve the problem?
Rather than attempting an assignment that TypeScript flags as incompatible, it constructs a clean, new object that TypeScript can confidently infer types for.
To fully understand this, read the explanation sections below.
Explanation of Issue
The error is happening at line
cleanedUser[field as keyof User] = user[field as keyof User];
During this assignment, TS has no way to understand that cleanedUser[field]
and user[field]
correspond to the same, specific data type from one specific key.
So, TS will only allow values that are compatible with all possible types of keys of cleanedUser
.
Lets quickly recall the types of cleanedUser
and user
The all possible types of keys of cleanedUser
are
number | undefined
string | undefined
Type that is compatible with both of the above types is undefined
. This is what TS allows here.
Hence the error:
Type 'string | number' is not assignable to type 'undefined'.
Type 'string' is not assignable to type 'undefined'.
Why does the error have 2 lines?
Line 1 shows the error, with the full types included. In line 2, it picks string
, only because it is the first constituent of the union type, to highlight a more specific example of one particular part of the overall type string | number
that is incompatible with the left-hand side (undefined
).
In this case, the second line does not really provide any extra info, and it might even be a little misleading, because both string
and number
are incompatible with undefined
on the left-hand side — string
is not the only problem. However, in many other TypeScript errors, it can be very helpful for TypeScript to slowly break down big types to highlight the incompatible parts.
Explanation of Solution
const cleanedUser: Partial<User> = Object.fromEntries(
Object.entries(user).filter(([userKey]) => !deletionMarkers[userKey as keyof User])
);
Object.entries(user)
: This turns theuser
object into an array of[key, value]
tuples. It's equivalent to[['id', 1], ['name', 'John Doe']]
..filter(([userKey]) => !deletionMarkers[userKey as keyof User])
: This applies the filter method from the Array prototype, which iterates over every element of the array and includes only those elements for which the provided callback function returns true.[userKey]
destructures the tuple passed to it and grabs the first element from each [key, value] tuple.Object.fromEntries(...)
: This function is essentially the reverse ofObject.entries(...)
. It receives an iterable (for instance, an array) of entries, where each entry is an array with a key-value pair, and transforms it back into an object. So,Object.fromEntries([['id', 1]])
results in{ id: 1 }
.
Reference
Answered By - Ash K
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.