Reputation: 1060
I'd like a method without parameters that normally returns an array, or just the first element of that array when I know there will only be one item.
Note: I cannot change the shape or values of Thing, it's from an external app, I'm just creating a wrapper around the properties of Thing.
interface Thing {
[key: string]: any;
}
class Getter <T> {
asArray: boolean;
things: Thing[];
prop: string;
constructor(prop: string, things: Thing[], asArray: boolean) {
this.asArray = asArray;
this.things = things;
this.prop = prop;
}
// THIS IS THE METHOD IN QUESTION
getProp(): T | T[] {
if (this.asArray) return things.map(t => t[this.prop]);
else return things[0][this.prop];
}
// MORE METHODS ARE HERE FOR CHECKING DATA
}
class ThingModel {
things: Thing[];
asArray: boolean;
constructor(asArray: boolean, ...things: Thing[]) {
this.asArray = asArray;
this.things = things;
}
get foo() {
// We know the values of Thing.foo are numbers
return new Getter<number>('foo', this.things, this.asArray);
}
get bar() {
// We know the values of Thing.foo are strings
return new Getter<string>('bar', this.things, this.asArray);
}
}
const allThings = new ThingModel(true, ...getAllThings());
const oneThing = new ThingModel(false, getOneThing());
// TypeScript cannot infer these types, and both are the union of
// T | T[] even though we know which it will be, so the caller is
// forced to still make other assertions or type casts.
const allFooProps = allThings.foo.getProp(); // we know it's number[]
const oneBarProp = oneThing.bar.getProp(); // we know it's a string
If I stick to always returning an array, some calls are not so intuitive.
// It's awkward that oneThing here would return an array of one property.
const [oneProp] = oneThing.foo.getProp();
I suppose I understand why this doesn't work out of the box - TypeScript likely cannot guarantee that Getter.asArray
doesn't get changed after the constructor, however, I've tried using a readonly property for asArray
as well, and it didn't help.
Overrides don't work because I have no parameters and I can't extend the class and override the method because it complains the return types aren't the same. I can't use generics, because I don't know the type yet at the constructor new Getter(...)
without first checking the ThingModel.asArray property, and ThingModel can't have a generic, because the types of individual properties are all different.
The only way I can think to get this working is to define a SingleGetter class and re-implment every method of Getter, and then in ThingModel
do an if/else for every property to construct the appropriate Getter or SingleGetter. I suppose that might be the most 'correct' way to do this - but it feels like a lot of excess code where Getter already has the right logic, just not a switch to return values
or values[0]
.
Is there an easier way to help the compiler know when I'm going to get a single value vs an array?
Upvotes: 0
Views: 1653
Reputation: 330086
In what follows, I'll be assuming that you're not better off refactoring to completely separate the "array" version of Getter
and ThingModel
from the "single" version.
You can make the asArray
properties a generic type A
that extends boolean
, so that the compiler keeps track of whether or not it is true
or false
.
So your Getter
class could look like:
class Getter<T, A extends boolean> {
asArray: A;
things: Thing[];
prop: string;
constructor(prop: string, things: Thing[], asArray: A) {
this.asArray = asArray;
this.things = things;
this.prop = prop;
}
getProp(): A extends true ? T[] : T;
getProp(): T | T[] {
if (this.asArray) return this.things.map(t => t[this.prop]);
else return this.things[0][this.prop];
}
}
where the return type of getProp()
is declared to be either T[]
or T
depending on the type of A
. Then your ThingModel
would need to also be generic in A
:
class ThingModel<A extends boolean> {
things: Thing[];
asArray: A;
constructor(asArray: A, ...things: Thing[]) {
this.asArray = asArray;
this.things = things;
}
get foo() {
return new Getter<number, A>('foo', this.things, this.asArray);
}
get bar() {
return new Getter<string, A>('bar', this.things, this.asArray);
}
}
You can see how we are just passing A
from ThingModel
into Getter
inside the foo()
and bar()
methods. And then your subsequent code will behave as you expect:
allThings.foo.getProp().map(x => x.toFixed());
oneThing.bar.getProp().toUpperCase();
Okay, hope that helps. Good luck!
Upvotes: 1