How can I write HasNumber
without typescript giving me an error because on HasNumberAndString
name is not a number
? I want a way to enforce HasNumberAndString
to have at least one property of type number
(without enforcing the property name), but be allowed to have properties of other types as well. Is this possible?
interface HasNumber {
[key: string]: number;
}
interface HasNumberAndString extends HasNumber {
age: number;
name: string;
}
What does ?: mean in TypeScript? Using a question mark followed by a colon ( ?: ) means a property is optional. That said, a property can either have a value based on the type defined or its value can be undefined .
To dynamically access an object's property: Use keyof typeof obj as the type of the dynamic key, e.g. type ObjectKey = keyof typeof obj; . Use bracket notation to access the object's property, e.g. obj[myVar] .
Use the Omit utility type to exclude a property from a type, e.g. type WithoutCountry = Omit<Person, 'country'> . The Omit utility type constructs a new type by removing the specified keys from the existing type. Copied!
To define an object of objects type in TypeScript, we can use index signatures with the type set to the type for the value object. const data: { [name: string]: DataModel } = { //... }; to create a data variable of type { [name: string]: DataModel } where DataModel is an object type.
There is no great way in TypeScript to allow a type with an index signature to have an additional property that doesn't match the index signature type. See microsoft/TypeScript#17867 for the suggestion to allow exceptions to index signatures, and maybe give it a 👍 to show support for it. For now, though, it's not directly supported.
One workaround is to use a type intersection instead of extending an interface:
type SortaHasNumberAndString = { [key: string]: number } & { age: number, name: string };
You're basically trying to fool the compiler since technically this means that the name
property should be both of type string
and number
, but you're hoping the compiler only notices the string
part. This actually does work when reading properties from a value of that type:
declare const sortaHasNumberAndString: SortaHasNumberAndString;
sortaHasNumberAndString.name; // string, not number
sortaHasNumberAndString.age; // number
sortaHasNumberAndString.numberOfLimbs; // number due to index signature
But doesn't work well when trying to assign to a value of that type:
// error, complains that name is not string & number
const notSoGreat: SortaHasNumberAndString = {
name: "Long John Silver",
age: 43,
numberOfLimbs: 3
}
So you're forced to use something like assertions or other tricks:
const notSoGreat = {
name: "Long John Silver",
age: 43,
numberOfLimbs: 3
} as any as SortaHasNumberAndString; // okay now
And that means you run into trouble with functions accepting this type:
declare function acceptSortaHasNumberAndString(x: SortaHasNumberAndString): void;
acceptSortaHasNumberAndString({name: "a", age: 123}); // error!
Not ideal.
Another solution (the better one in my opinion) is to allow HasNumber
and HasNumberAndString
to be generic in the key with a number property. This means that there is no index signature to worry about.
type HasNumber<K extends keyof any> = Record<K, number>;
type HasNumberAndString<K extends keyof any> = { name: string } & HasNumber<Exclude<K, "name">>;
This uses the built-in Record
and Exclude
type aliases to say that HasNumberAndString<K>
should have a name
property of type string
, but all other properties should be of type number
. You can modify those to reflect different constraints if you need to. Let's see how it works:
If you want to talk about a specific value of type HasNumberAndString
you need to specify the keys in the type:
const hns: HasNumberAndString<'age'> = {
name: "Harry James Potter",
age: 38
}
That might not be great, but in most cases you can just let TypeScript infer those keys for you:
const inferHasNumberAndString =
<T>(x: T & HasNumberAndString<keyof T>): HasNumberAndString<keyof T> => x;
const hns = inferHasNumberAndString({
name: "Harry James Potter",
age: 38
}); // inferred as type HasNumberAndString<"name" | "age">;
The above helper function inferHasNumberAndString
is generic and shows the general method by which you can accept values of type HasNumberAndString<K>
without enforcing K
yourself:
declare function acceptHasNumberAndString<T>(x: T & HasNumberAndString<keyof T>): void;
acceptHasNumberAndString({ name: "Hermione Granger", age: 39 });
Hope that helps; good luck!
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