Just like the previous response mentioned, TypeScript currently lacks support for a type where certain properties act as exceptions to the index signature. This means that there is no straightforward way to define your MyDictionary
as a consistent concrete type. While using an inconsistent-intersection solution such as
{[k: string]: string]} & {id: number}
might work for property reads, it becomes cumbersome when dealing with property writes.
In the past, there was a proposal to introduce "rest" index signatures which would allow specifying that an index signature should encompass all properties except those explicitly listed. Unfortunately, this suggestion did not come to fruition.
More recently, there were discussions around implementing negated types and arbitrary key types for index signatures in TypeScript. These enhancements could potentially enable representing exception/default index signature properties by utilizing syntax like
{ id: number; [k: string & not "id"]: string }
. However, as of TypeScript 3.5, this feature is still under development and may never be implemented.
Therefore, accurately representing MyDictionary
as a concrete type remains challenging. Nevertheless, you can represent it as a generic constraint using TypeScript's generics functionality. Bear in mind that transitioning to this approach would require converting your existing concrete functions into generic ones and adjusting your values accordingly, which might introduce unnecessary complexity. Here's how you can implement it:
type MyDictionary<T extends object> = { id: number } & {
[K in keyof T]: K extends "id" ? number : string
};
The above code snippet demonstrates defining MyDictionary<T>
to transform a given type T
into a structure that aligns with the desired MyDictionary
type. Additionally, a helper function named asMyDictionary
validates whether an object matches the defined structure.
const asMyDictionary = <T extends MyDictionary<T>>(dict: T) => dict;
You can utilize the asMyDictionary()
function to assess if an object conforms to the expectations of MyDictionary<T>
, as illustrated below:
const otherValues = {
some: "some",
other: "other",
values: "values"
};
const composedDictionary = asMyDictionary({
id: 1,
...otherValues
}); // This statement is valid
The example above compiles without errors since the input parameter satisfies the requirements of MyDictionary<T>
. Let's now explore some scenarios where mismatches occur:
const invalidDictionary = asMyDictionary({
id: 1,
oops: 2 // Error - Number is not a string
})
const invalidDictionary2 = asMyDictionary({
some: "some" // Error - Property 'id' is missing
})
const invalidDictionary3 = asMyDictionary({
id: "oops", // Error - String is not a number
some: "some"
})
By attempting these variations, the TypeScript compiler can pinpoint inconsistencies in the objects and provide detailed error messages.
As of TypeScript 3.5, this implementation represents the closest approximation to your requirement. Hopefully, this information proves useful to you. Best of luck on your coding endeavors!
Link to code