I am creating a simple function to remove duplicates from an array in typescript.
I know there are many different ways to achieve the goal, but my end goal is to learn how types work, so I don't need a solution that differs from my existing code.
function removeDuplicates(arr, propName) {
const newArr = [];
const lookup = {};
for (let i in arr) {
lookup[arr[i][propName]] = arr[i];
}
for (let i in lookup) {
newArr.push(lookup[i]);
}
return newArr;
}
I am trying to convert that function to Typescript, but stuck at declaring types for lookup variable.
Here is my typescript code:
function removeDuplicates<T, K extends keyof T>(arr: T[], propName: K) {
const newArr: T[] = [];
const lookup: Partial<Record<T[K], T>> = {};
^^^^ here is the error
for (let i in arr) {
lookup[arr[i][propName]] = arr[i];
}
for (let i in lookup) {
newArr.push(lookup[i]);
}
return newArr;
}
Type 'T[K]' does not satisfy the constraint 'string | number | symbol'
I know why I am getting the error. I am getting the error because value of object.key can be anything. But in my use case, I want to restrict the developer to pass in only the key whose value is either string | number. But I don't know how to do that in typescript.
As you said, you're getting the error because while K is constrained to be a key from the array element type T, there is nothing saying that the property at that key T[K] is itself key-like:
function removeDuplicates<T, K extends keyof T>(arr: T[], propName: K) {
const lookup: Partial<Record<T[K], T>> = {}; // error!
}
removeDuplicatesOops([{ a: new Date(), b: "hey" }], "a"); // no error
In TypeScript, a type is "keylike" if it's assignable to string | number | symbol, which is given a convenient alias PropertyKey.
We can't constrain T[K] directly, but we can constrain K and/or T so that T[K] is effectively constrained. The easiest way to get your desired behavior where the compiler realizes that Partial<Record<T[K], T>> is an acceptable thing is to constrain T so that the property type at the keys in K must be assignable to PropertyKey:
function removeDuplicates<T extends Record<K, PropertyKey>, K extends keyof T>(
arr: T[], propName: K) {
const lookup: Partial<Record<T[K], T>> = {};
// okay
}
removeDuplicates([{ a: new Date(), b: "hey" }], "a");
// ---------------> ~
// Type 'Date' is not assignable to type 'PropertyKey'.
Yes, the constraints on T and K are circular, but in an allowable way (although it's sometimes tricky to avoid circularity warnings in situations like this). Note that the error appears on the keys of the arr argument and not the propName argument itself. If you really need the latter then you can constrain K even further like
function removeDuplicates<
T extends Record<K, PropertyKey>,
K extends keyof { [P in keyof T as T[P] extends PropertyKey ? P : never]: any }
>(arr: T[], propName: K) {
const lookup: Partial<Record<T[K], T>> = {};
// okay
}
removeDuplicates([{ a: new Date(), b: "hey" }], "a"); // error!
// -------------------------------------------> ~~~
//Argument of type '"a"' is not assignable to parameter of type '"b"'.
But this might be more complicated than you are looking for (and it would take me too far afield to explain how that K constraint works).
Playground link to code
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With