Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Typescript: object with at least one property of type T

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;
}
like image 615
Felipe Avatar asked Oct 30 '18 02:10

Felipe


People also ask

What does ?: Mean in TypeScript?

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 .

How do you access the properties of objects in TypeScript?

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] .

How do you omit a property in TypeScript?

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!

How do you define object of objects type in TypeScript?

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.


1 Answers

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!

like image 109
jcalz Avatar answered Oct 16 '22 08:10

jcalz