Reputation: 1164
interface ContextValue {
[K: string]: {
value: any
error: boolean
errorMessage: string
}
}
export interface GatheringExpChildMachineBaseContext<TYPE> extends Omit<ContextValue, 'id' | 'originalData' | 'order'> {
id?: string
originalData: TYPE
order: number
}
I get the error message TS 2411 Property 'id' of type 'string' is not assignable to string index type '{ value: any; error: boolean; errorMessage: string; }'.
, and the equivalent messages for originalData
and order
.
This seems incorrect, as Omit should grab the keys of the ContextValue
interface (which is any key,) and omit the union types I supplied 'id' | 'originalData' | 'order'
and allow me to override them.
Upvotes: 1
Views: 502
Reputation: 328132
TypeScript does not currently support subtraction/negated types, which is what you'd need to say "string
but not ("id" | "originalData" | "order")
". Omit
is implemented by using the Exclude<T, U>
utility type on the keys. Exclude<T, U>
filters any union members of T
which are subtypes of U
. Since string
is not a union, the filter acts on string
as a whole. And since string
is not a subtype of a finite union of string literals (it's a supertype, not a subtype), the value of Exclude<string, "id" | "originalData" | "order">
is just string
. In some sense, the current version of TypeScript thinks "string
minus ("id" | "originalData" | "order")
equals string
".
The specific functionality you're looking for, where you can make exceptions to an index signature, is the subject of an open issue in GitHub: microsoft/TypeScript#17867. If you want to see this implemented in TypeScript you might want to go to that issue and give it a 👍. Its also marked "awaiting more feedback" so if you think you have a compelling use case for it that isn't covered already, you might want to make a comment detailing it.
For now there are only various workarounds, each of which have an associated pain point.
You can use an intersection to avoid the error, which makes it easy to use an existing value of that type:
interface Extension<T> { id?: string, originalData: T, order: number }
type GECMBCIntersection<T> = ContextValue & Extension<T>
Which makes it easy to use an existing value of that type:
declare const i: GECMBCIntersection<number>;
i.id?.toUpperCase(); // okay
i.originalData.toFixed(); // okay
i.order.toFixed(); // okay
i.somethingElse.errorMessage.toUpperCase(); // okay
But it's not easy to verify that a value is of that type, for the same not-compatible-with-index-signature reason:
const iBad: GECMBCIntersection<number> = {
originalData: 1,
order: 2
} // error! originalData is not assignable to index signature
You can represent it as a generic constraint in the actual keys instead of as a concrete type with an index signature:
type GECMBCConstraint<K extends PropertyKey, T> =
Record<Exclude<K, keyof Extension<any>>, ContextValue[string]> & Extension<T>;
But then you need to carry an extra generic type parameter everywhere and use helper functions to create instances without redundant key names:
const asGECMBCConstraint = <G extends GECMBCConstraint<keyof G, any>>(g: G) => g;
const cGood = asGECMBCConstraint({
originalData: 1,
order: 2,
somethingElse: {
value: "blork",
error: true,
errorMessage: "blork!!",
}
})
And even then it's hard to work with an actual value when you want to access the erstwhile-index-signature properties:
function cBad<G extends GECMBCConstraint<keyof G, any>>(g: G) {
if ("somethingElse" in g) {
g.somethingElse; // error
}
const asserted = g as GECMBCIntersection<any>;
if ("somethingElse" in asserted) {
asserted.somethingElse.errorMessage.toUpperCase() // okay
}
}
For now I usually recommend refactoring the code so as not to mix index signatures with other properties. If you can do so, rewrite your type so that it has a ContextValue
instead of so that it is a ContextValue
. (This is also known as composition over inheritance). Then you get the simpler interface:
interface Easier<T> extends Extension<T> {
contextValues: ContextValue;
}
which is easy for the compiler to reason about:
const easy: Easier<number> = {
originalData: 1,
order: 2,
contextValues: {
somethingElse: {
value: "blork",
error: true,
errorMessage: "blork!!",
}
}
};
Okay, hope that helps; good luck!
Upvotes: 2