TypeScript Version: nightly 2.1-20160914
I have a base class that takes a generic parameter with a constraint that is an index signature. Something like
interface Constraint {
[key: string]: number
}
class Base<T extends Constraint> {
entity: T
}
This class is meant to be inherited from and provided with another type that satisfies the index signature but doesn't carry it itself. Something like
interface MyType {
prop: number;
label: number;
code: number
}
class MyClass extends Base<MyType> {
// Error: Index signature is missing from MyType
}
I don't want to put the index signature on MyType because destructuring entity won't output an error when the property doesn't exist:
const {id} = this.entity // oops, no error
which is a critical feature for me.
I don't want to remove the index signature from the constraint because the base class completely revolves around it and I would lose a lot of type safety.
Is there something I missed? Should we have some new language feature to solve this issue?
(Now that I think of it I suppose it's a generic assignability issue between types, not restricted of type constraints, and it's not solved by #7029 since it's only about actual litterals, not types)
The problem with not having the index signature is that your class becomes unsound to basic subtyping. Consider:
interface Stuff {
age: number;
}
interface StuffWithName extends Stuff {
name: string;
}
interface Constraint<T> {
// [key: string]: T;
}
class Base<T extends Constraint<T>> {
setT(x: T) { }
}
class Derived extends Base<Stuff> {
}
let d = new Derived();
let s: StuffWithName = { age: 10, name: '' };
d.setT(s);
Well, I definitely want to keep the index signature around, but at the same time I don't want every derived type to be forced to implement it. Like something that's defined somewhere up in the hierarchy of types that's properly restricting any derived type without having them carrying more information than they should. I don't know exactly how the type system works internally, but I'm pretty sure there's no such thing as an "inheritance chain for types", am I right?
Coming back to the realm of possibilities, how would you address my problem then? Am I really limited to one of the two propositions I made (no index signature or index signature everywhere)?
You could write
interface MyType extends Constraint {
prop: number;
label: number;
code: number
}
to save the trouble of typing the index signature. Thoughts?
Typing the index signature isn't a problem at all, my problem is more about the fact my derived type carries it.
Maybe I should tell you more about what I am trying to achieve:
My base class is a React component that's supposed to take some kind of store as parameter, wrap it up in some viewmodel logic, and make it up for grabs for a derived class to use it.
It looks like this
interface StoreItem<T> {
metadata: {},
value: T
}
interface Store {
[key: string]: StoreItem<any>
}
abstract class Base<P, E extends Store> extends React.Component<P, void> {
viewModel: E;
constructor(props, store: E) {
this.viewmodel = createViewModel(store):
}
renderProp(prop: StoreItem<any>) {
// Something something rendering
}
}
interface MyStore {
prop1: StoreItem<string>;
prop2: StoreItem<number>;
}
class MyClass extends Base<{}, MyStore> {
constructor(props) { super(props, myStore); }
render() {
const {prop1, prop2} = this.viewModel;
return <div>{this.renderProp(prop1)}{this.renderProp(prop2)}</div>;
}
}
That's the essence of what I'm trying to do. This doesn't work because MyStore doesn't have Store's index signature.
All those stores are generated from my server-side model, and I want to be able to safely modify this model, meaning if that one property is removed/changes, my app would stop compiling.
Let's say MyStore does inherit the index signature from Store, and then I do some refactorings that renames prop1 to name and prop2 to surname. The above code would still compile and still be typed alright because of the index signature, but at runtime it would crash because those properties don't exist anymore. I already made a similar thing that I extensively use in a project at work and I cannot stress enough how important that feature is for me.
Removing the index signature altogether could make this work, but at the expense of losing all typechecking in the base class and making the contract of the class implicit (how am I suppose to enforce that the type parameter should follow an index signature that's not there?).
This has been a problem for me as well. I think there needs to be a way to differentiate "handles objects with any key as long as the value has type T" and "requires object that can handle any key as long as the type is T"
I had a similar problem and here is how I "solved" it:
I have a basic interface with index signature, e.g.:
interface StringOnlyProps {
[name: string]: string;
}
I also have a generic class that takes above types:
class DoSomethingWithStringOnlyProps<T extends StringOnlyProps> {...}
Now, here is how I did it:
All my other interfaces do not extend the constraint interface as I don't want to introduce "dynamic" properties / index signature in them, e.g.:
interface X { ... }
interface Y { ... }
However, I defined an interface whose sole purpose is to break compilation if some of X,Y,... does not conform to StringOnlyProps:
interface Constraint implements X, Y, StringOnlyProps { } // empty
I even changed the generic above to require presence of such a constraint:
class DoSomethingWithStringOnlyProps<T, C extends StringOnlyProps & T> {
// C is unused here but its existence guarantees T and StringOnly are compatible
}
Now, if you change a type - Constraint would not compile.
If you rename a property - code that uses X, Y or whatever, would not compile as they do not anymore provide index signature.
@RyanCavanaugh did you have a chance to think about the issue?
@JabX have you found a way around the issue, other than what @rado-nikolov suggested?
@RyanCavanaugh @JabX
I noticed an interesting thing. If try to pass an declared earlier interface as a generic, then compiler complains about it. If I declare the interface in place, then there's no compilation error.
const func = <T extends { [index: string]: string }>(a: T) => a
func<{ test: string }>({ test: '123' }) // works
interface A {
test: string
}
func<A>({ test: '123' }) // fails
Playground
Tested with typescript 3.0-rc
Found a workaround to have an index signature constraint for function parameters, but not generic itself
interface A {
a: string,
b: string
}
type AConstraint<T extends A> = T & { [index: string]: string }
const a = <T extends A>(props: AConstraint<T>): T => props
const propsTest1: A = { a: '1', b: '2' }
a(propsTest1 as AConstraint<A>)
const propsTest2 = { a: '1', b: '2', c: 'ftdrff' }
a(propsTest2)
@andy-ms sorry for disturbing you directly, but could someone from TypeScript team take a look at the issue? It was marked as 'Needs investigation' almost one and a half years ago. Thank you!
@keenondrums I ran into the same issue with you and after reading this thread over and over + many trials and errors, here is a working solution. Thanks for starting the discussion.
type ValueIsNumber<T> = {
[key in keyof T]: number;
}
class Base<T extends ValueIsNumber<T>> {
entity: T
}
interface MyType {
prop: number;
label: number;
code: number
}
class MyClass extends Base<MyType> {
// works!
}
@kristw thank you! Yeah, using mapped types solves the issue, until I stumble upon a case when it doesn't :)
This issue has been marked 'Working as Intended' and has seen no recent activity. It has been automatically closed for house-keeping purposes.
Most helpful comment
@keenondrums I ran into the same issue with you and after reading this thread over and over + many trials and errors, here is a working solution. Thanks for starting the discussion.