Is there a way to extend the built-in Record (or a { [key:string]: string } interface) where you also define some fixed keys and their types?
Let's say we have this:
const otherValues = {
some: 'some',
other: 'other',
values: 'values',
}
const composedDictionary = {
id: 1,
...otherValues
}
I want to define an interface for composedDictionary where id
is typed as number
(and number only) and everything else as string
.
I've tried this:
interface ExtendedRecord extends Record<string, string> {
id: number;
}
and this:
interface MyDictionary {
[key: string]: string;
id: number;
}
Both fail with:
Property 'id' of type 'number' is not assignable to string index type 'string'
Any ideas?
Ideally the index signature should reflect any possible indexing operation result. If you access composedDictionary
with an uncheckable string key the result might be number
if that string
is actually 'id'
(eg: composedDictionary['id' as string]
, the typings would say this is string
but at runtime it turns out to be number
). This is why the type system is fighting you on this, this is an inconsistent type.
You can define your index to be consistent will all properties:
interface MyDictionary {
[key: string]: string | number;
id: number;
}
There is a loophole to the checks typescript does for index and property consistency. That loop hole is intersection types:
type MyDictionary = Record<string, string> & {
id: number
}
const composedDictionary: MyDictionary = Object.assign({
id: 1,
}, {
...otherValues
});
The compiler will still fight you on assignment, and the only way to create such an inconsistent object in the type system is by using Object.assign
As the other answer said, TypeScript does not support a type in which some properties are exceptions to the index signature... and thus there is no way to represent your MyDictionary
as a consistent concrete type. The inconsistent-intersection solution ({[k: string]: string]} & {id: number}
) happens to work on property reads, but is difficult to use with property writes.
There was an old suggestion to allow "rest" index signatures where you can say that an index signature is supposed to represent all properties except for those specified.
There's also a more recent (but possibly shelved) pair of enhancements implementing negated types and arbitrary key types for index signatures, which would allow you to represent such exception/default index signature properties as something like { id: number; [k: string & not "id"]: string }
. But that doesn't compile yet (TS3.5) and may never compile, so this is just a dream for now.
So you can't represent MyDictionary
as a concrete type. You can, however, represent it as a generic constraint. Using it suddenly requires that all your previously concrete functions must become generic functions, and your previously concrete values must become outputs of generic functions. So it might be too much machinery than it's worth. Still, let's see how to do it:
type MyDictionary<T extends object> = { id: number } & {
[K in keyof T]: K extends "id" ? number : string
};
In this case, MyDictionary<T>
takes a candidate type T
and transforms it into a version which matches your desired MyDictionary
type. Then we use the following helper function to check if something matches:
const asMyDictionary = <T extends MyDictionary<T>>(dict: T) => dict;
Notice the self-referencing constraint T extends MyDictionary<T>
. So here's your use case, and how it works:
const otherValues = {
some: "some",
other: "other",
values: "values"
};
const composedDictionary = asMyDictionary({
id: 1,
...otherValues
}); // okay
That compiles with no errors, because the parameter to asMyDictionary()
is a valid MyDictionary<T>
. Now let's see some failures:
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"
})
The compiler catches each of those mistakes and tells you where the problem is.
So, this is the closest I can get to what you want in as of TS3.5. Okay, hope that helps; good luck!
Link to code
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With