Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Intersecting keys in Typescript

I would like to write a type-safe utility function for comparison of properties of 2 given objects in Typescript 4.0. My initial attempt is as following:

export function propsAreEqual<O extends object, T extends O, S extends O>(first: T, second: S, props: (keyof O)[]) {
    return props.every(prop => first[prop] === second[prop])
}

However, I get a compile error TS2367 with this approach, which states that: This condition will always return 'false' since the types 'T[keyof O]' and 'S[keyof O]' have no overlap.

That error seems counter-intuitive to me. If both T and S extend object of type O then shouldn't they both necessarily contain all keys of type O? I would greatly appreciate if anyone could clarify what am I missing here and if there is a more sound approach to what I am trying to achieve?

like image 762
arslancharyev31 Avatar asked May 08 '26 17:05

arslancharyev31


2 Answers

The error you are getting is not incorrect, extending O just means extends object, and in turn this means you could have T = { a: number } and O = { a: string } (ex). The key is the same, but there is no overlap between T[keyof O] and O[keyof O]. There could be overlap, but when dealing with TS expects to be able to prove the function is correct for any valid instantiation of the type parameter, and as we see here there can be cases where this function is not valid.

You could have several approaches to defining this function you could use a single type parameter for the first object, and define the second object as a Pick or the passed in properties.

function propsAreEqual<T extends object, K extends keyof T>(first: T, second: Pick<T, K>, props: K[]) {
    return props.every(prop => first[prop] === second[prop])
}

Playground Link

This version will ensure that T[K] is the same type for both parameters. The disadvantage of this version is that if you try to pass in an object literal as the second parameter, excess property checks will kick in. Also another disadvantage is that intellisense will suggest all properties of T in the third parameter, and if the property is not common, you will get an error on the second parameter.

Personally I would prefer an option that sacrifices full type checking in the function in return for fixing the two problems above. This would be this version:

function propsAreEqual<T extends object,  S extends Pick<T, K>,  K extends keyof T & keyof S>(first: T, second: S, props: K[]) {
    return props.every(prop => first[prop] === (second as Pick<T, K>)[prop])
}

Playground Link

like image 81
Titian Cernicova-Dragomir Avatar answered May 11 '26 07:05

Titian Cernicova-Dragomir


When you infer a type parameter, Typescript wants to infer it from just one of the arguments; so the signature propsAreEqual<T>(a: T, b: T, props: (keyof T)[]) doesn't work because Typescript infers T from the type of the first argument and then doesn't try to loosen it when it finds that the second argument isn't assignable to that type.

Solution: use a generic type which will be inferred from props instead of from a or b.

function propsAreEqual<K extends PropertyKey>(a: Record<K, unknown>, b: Record<K, unknown>, props: K[]) {
    return props.every(prop => a[prop] === b[prop]);
}

Playground Link

like image 32
kaya3 Avatar answered May 11 '26 06:05

kaya3