Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

typescript Constraining generics by compatible property types

Tags:

typescript

I'm writing a function which updates property in one object with some other property from another object

like

function compareAndUpdate<T, S>(target: T, source: S, targrtKey: keyof T, sourceKey: keyof S): boolean {
    // some processing
    target[targrtKey] = source[sourceKey]; // this line gives error 

    // some more processing

}

I know that I can fix error by using <any>

target and source are of distinct types. T and S may or may not have proprieties with same name , but T can have a property whose type is same as some other property in S

Is there a way to guaranty that typeof target[targrtKey] === typeof source[sourceKey] so that compiler catch error when incompatible property keys are passed.

Edit: Submitted upstream feature request here

like image 601
AbdulKareem Avatar asked Mar 05 '26 00:03

AbdulKareem


2 Answers

You can, in fact, do this. Here's how:

function compareAndUpdate<T, TK extends keyof T, SK extends string, S extends Record<SK,T[TK]>>(target: T, source: S, targetKey: TK, sourceKey: SK): boolean {
    // some processing
    target[targetKey] = source[sourceKey]; // okay
    return true;
}

Let's unpack that declaration:

  • T is the type of the target
  • TK is the type of the target key (so it needs to extend keyof T). Note that T[TK] is the type of the target value for that key.
  • SK is the type of the source key (so it needs to extend string)
  • S is the type of source, which needs to have SK as a key, and any value you get from that key needs to be assignable to T[TK]. Record<SK,T[TK]> is a type with keys of type SK, and values of type T[TK]. So when S extends Record<SK,T[TK]>, we're saying that we want S to be any type where S[SK] is the same as T[TK].

Let's try it:

interface Person {
  name: string;
  age: number;
}
const person: Person = { name: 'Stephen King', age: 69 };

interface Book {
  title: string;
  authorName: string;
}
const book: Book = { title: 'The Running Man', authorName: 'Richard Bachman' };

compareAndUpdate(book, person, 'authorName', 'name'); // okay
compareAndUpdate(book, person, 'authorName', 'age');  // error, number not assignable to string
compareAndUpdate(person, book, 'authorName', 'name'); // error, 'name' property not found

Does that work for you?

like image 52
jcalz Avatar answered Mar 06 '26 12:03

jcalz


You could just use the same type if you want them to be equal :

function compareAndUpdate<T>(target: T, source: T, targrtKey: keyof T, sourceKey: keyof T): boolean { return true; }

Another version would be to guarantee that no property of S is not present in T

function compareAndUpdate<T extends S, S>(target: T, source: S, targrtKey: keyof T, sourceKey: keyof S): boolean { return true; }

When you use you will get errors if someone tries to put on the target, properties not part of T

//Valid
compareAndUpdate({ test: "S"}, { test: "DD" }, "test", "test")
compareAndUpdate({ test: "S", bla : 0}, { test: "DD" }, "test", "test")
//Invalied, bla does not exist on T
compareAndUpdate({ test: "S"}, { test: "DD", bla : 0 }, "test", "test")

I use object literals for simplicity, but it will work with classes as well, if S contains fields in the class :

class Dog {
  name: string;
  bark(): void{}
}
// OK
compareAndUpdate(new Dog(), {name: "" }, "name", "name");
// NOK
compareAndUpdate(new Dog(), {isAGodBoy: "" }, "name", "name");
like image 44
Titian Cernicova-Dragomir Avatar answered Mar 06 '26 12:03

Titian Cernicova-Dragomir



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!