In Typescript, I have two interfaces A
and B
, both of them having the method .toString()
on all of their values. I want to write a class Getter
that can take a property name key
on construction, and then later be passed an object thing
conforming to one of these interfaces, and return thing[key].toString()
. I want to do this type-safely, so that if key
does not exist on thing
, I get an error.
(Note that each Getter
will only ever be passed objects of a single interface, known at compile time.)
I can't figure out how to do this: I tried having the type parameter extend A | B
where A
and B
are the two interfaces this class needs to be used on, but that runs into issues, as we could define an interface C
that extends A
but does not have .toString()
. I then tried having it extend Record<string, Stringable>
(where Stringable
is an interface consisting only of { toString: () => string }
) but this doesn't work because Record
implies it's acting as a dictionary, which it isn't. I've also tried parameterizing it over a Field
type that extends string
, but I'm not sure if this would work given that I wouldn't know the value of Field
at compile time.
Is this actually possible to do in typescript? If not, are there any alternative methods to what I'm trying to achieve?
(Note also that this is a toy example; in my actual code the class needs to perform an expensive computation on construction and then will be passed tons of objects in a loop, which is why its structured this way.)
Code Example
What I'm trying to do
interface Stringable {
toString: () => string
}
interface A {
a: number
}
interface B {
b: Date
}
class Getter<AB extends /* some type here */> {
key: keyof AB;
constructor(key: keyof AB) {
this.key = key;
}
get(thing: AB) {
return thing[this.key].toString();
}
}
const bGetter = new Getter<B>('b');
bGetter.get({b: new Date(0,0)});
// "Mon Jan 01 1900 00:00:00 GMT-0800 (Pacific Standard Time)"
const aGetter = new Getter<A>('b');
// type error
bGetter.get({a: 105});
// type error
First Failure
Changing /* some type here */
to A | B
doesn't work because if AB extends A | B
then {a: number, c: undefined}
satisfies AB
.
Second Failure
Changing /* some type here */
in the above to Record<string, Stringable>
doesn't work because neither A
nor B
are compatible with that type.
CodePudding user response:
I believe you can just do:
class Getter<AB extends Record<keyof AB, Stringable>>
Not in a position to test, so let me know if it works or not.