I'm trying make a generic Typescript function to check if an array contains duplicates. For example:
interface Student {
name: string;
class: string;
};
const students: Student[] = [
{ name: 'John Smith', class: 'Science' },
{ name: 'Edward Ryan', class: 'Math' },
{ name: 'Jessica Li', class: 'Social Studies'},
{ name: 'John Smith', class: 'English'}
];
That is the data.
This is what I want to do with the data:
const registerStudents = async (students: Student[]): Promise<void> {
checkDuplicate(students, existingState); //This is the function I want to build
const response = await axios.post('/students/new', students)
existingState.push(response); //pushes newly registers students to the existing state
};
Regarding the checkDuplicate()
, I want to make it a generic function, but I'm struggling with the logic.
export const checkDuplicate = <T>(items: T[], existingState: T[]): void {
//checks if the items have any duplicate names, in this case, it would be 'John Smith', and if so, throw an error
//Also checks if items have any duplicate names with the existingState of the application, and if so, throw an error
if (duplicate) {
throw new Error('contains identical information')
};
};
It's a little bit complex and I haven't been able to figure out the logic to work with typescript. Any advice on how I can implement this would be appreciated!
CodePudding user response:
One reasonable way to approach this is to have checkDuplicate()
take a single array items
of generic type T[]
, and another array keysToCheck
of type K[]
, where K
is a keylike type (or union of keylike types) and where T
is a type with keys in K
and whose values at those keys are string
s. That is, the call signature of checkDuplicate()
should be
declare const checkDuplicate: <T extends Record<K, string>, K extends PropertyKey>(
items: T[],
keysToCheck: K[]
) => void;
This function should iterate over both items
and keysToCheck
, and if it finds an item where a property is the same string as the same property in a previous item, it should throw an error.
If you had such a function, you could write the version which accepts students
and existingState
, two arrays of Student
objects, like this:
function checkDuplicateStudents(students: Student[], existingState: Student[]) {
checkDuplicate([...students, ...existingState], ["name", "class"]);
}
where we are spreading the students
and existingState
arrays into a single array to pass as items
to checkDuplicate()
, and since we are checking Student
we are passing ["name", "class"]
as keysToCheck
.
Here's a possible implementation of checkDuplicate()
:
const checkDuplicate = <T extends Record<K, string>, K extends PropertyKey>(
items: T[],
keysToCheck: K[]
): void => {
const vals = {} as Record<K, Set<string>>;
keysToCheck.forEach(key => vals[key] = new Set());
for (let item of items) {
for (let key of keysToCheck) {
const val: string = item[key];
const valSet: Set<string> = vals[key]
if (valSet.has(val)) {
throw new Error(
'contains identical information at key "'
key '" with value "' val '"');
};
valSet.add(val);
}
}
}
The way it works is that we create an object named vals
with one key for each element key
of keysToCheck
. Each element vals[key]
is the Set
of strings we have already seen for that key
. Every time we see a string
-valued property val
with key key
in any item
in the items
array, we check whether the set in vals[key]
has val
. If so, we've seen this value for this key before, so we throw an error. If not, we add it to the set.
(Note that it's possible to replace Set<string>
with a plain object of the form Record<string, true | undefined>
, as shown in Mimicking sets in JavaScript? , but I'm using Set
here for clarity.)
Okay, let's test it against your example:
checkDuplicateStudents(students, []);
// contains identical information at key "name" with value "John Smith"
Looks good. It throws an error at runtime and properly identifies the duplicate data.