Typescript object with string properties

Template Literal Types

Template literal types build on string literal types, and have the ability to expand into many strings via unions.

They have the same syntax as template literal strings in JavaScript, but are used in type positions. When used with concrete literal types, a template literal produces a new string literal type by concatenating the contents.

When a union is used in the interpolated position, the type is the set of every possible string literal that could be represented by each union member:

For each interpolated position in the template literal, the unions are cross multiplied:

We generally recommend that people use ahead-of-time generation for large string unions, but this is useful in smaller cases.

The power in template literals comes when defining a new string based on information inside a type.

Consider the case where a function ( makeWatchedObject ) adds a new function called on() to a passed object. In JavaScript, its call might look like: makeWatchedObject(baseObject) . We can imagine the base object as looking like:

The on function that will be added to the base object expects two arguments, an eventName (a string ) and a callback (a function ).

The eventName should be of the form attributeInThePassedObject + «Changed» ; thus, firstNameChanged as derived from the attribute firstName in the base object.

The callback function, when called:

  • Should be passed a value of the type associated with the name attributeInThePassedObject ; thus, since firstName is typed as string , the callback for the firstNameChanged event expects a string to be passed to it at call time. Similarly events associated with age should expect to be called with a number argument
  • Should have void return type (for simplicity of demonstration)

The naive function signature of on() might thus be: on(eventName: string, callback: (newValue: any) => void) . However, in the preceding description, we identified important type constraints that we’d like to document in our code. Template Literal types let us bring these constraints into our code.

Notice that on listens on the event «firstNameChanged» , not just «firstName» . Our naive specification of on() could be made more robust if we were to ensure that the set of eligible event names was constrained by the union of attribute names in the watched object with “Changed” added at the end. While we are comfortable with doing such a calculation in JavaScript i.e. Object.keys(passedObject).map(x => `$Changed`) , template literals inside the type system provide a similar approach to string manipulation:

With this, we can build something that errors when given the wrong property:

Inference with Template Literals

Notice that we did not benefit from all the information provided in the original passed object. Given change of a firstName (i.e. a firstNameChanged event), we should expect that the callback will receive an argument of type string . Similarly, the callback for a change to age should receive a number argument. We’re naively using any to type the callback ’s argument. Again, template literal types make it possible to ensure an attribute’s data type will be the same type as that attribute’s callback’s first argument.

The key insight that makes this possible is this: we can use a function with a generic such that:

  1. The literal used in the first argument is captured as a literal type
  2. That literal type can be validated as being in the union of valid attributes in the generic
  3. The type of the validated attribute can be looked up in the generic’s structure using Indexed Access
  4. This typing information can then be applied to ensure the argument to the callback function is of the same type

Here we made on into a generic method.

When a user calls with the string «firstNameChanged» , TypeScript will try to infer the right type for Key . To do that, it will match Key against the content before «Changed» and infer the string «firstName» . Once TypeScript figures that out, the on method can fetch the type of firstName on the original object, which is string in this case. Similarly, when called with «ageChanged» , TypeScript finds the type for the property age which is number .

Inference can be combined in different ways, often to deconstruct strings, and reconstruct them in different ways.

Intrinsic String Manipulation Types

To help with string manipulation, TypeScript includes a set of types which can be used in string manipulation. These types come built-in to the compiler for performance and can’t be found in the .d.ts files included with TypeScript.

Converts each character in the string to the uppercase version.

Источник

Typescript object with string properties

You might try to override the type of a specific property with an index signature.

Copied!
interface Person [index: string]: string; // ⛔️ Error: Property 'age' of type 'number' // is not assignable to 'string' index type 'string'. age: number; >

But the type of the age property in the example doesn’t match the type of the string index, so TypeScript gives us an error.

In this situation, you can set the type of the string index to a union.

Copied!
interface Person [index: string]: string | number; age: number; name: string; > // 👇️ const p1: Person const p1: Person = name: 'Tom', age: 30, country: 'Chile' >;

The type of the values with an index signature of type string is a union of string and number .

The age and name properties have a value that is a subtype of the union, so we don’t get an error.

Trying to add a property to the type that is not in the union, would cause an error.

Copied!
interface Person [index: string]: string | number; age: number; name: string; // ⛔️ Error: Property 'languages' of type 'string[]' is // not assignable to 'string' index type 'string | number'. languages: string[]; >

# Narrowing down the type for specific properties

If the value of the string keys were set to any in the index signature, you could add properties to the object of any type, since anything is more specific than any .

Copied!
interface Person [index: string]: any; age: number; name: string; languages: string[]; > // 👇️ const p1: Person const p1: Person = name: 'Bobby Hadz', age: 30, country: 'Chile', languages: ['english', 'spanish'], >;

This would be a good way to narrow down the type of some of the properties that you know ahead of time.

For example, if I try to add an age property to the object that has a type of string , the type checker would throw an error because it expects a number.

Copied!
interface Person [index: string]: any; age: number; name: string; languages: string[]; > // 👇️ const p1: Person const p1: Person = name: 'Bobby Hadz', // ⛔️ Error: Type 'string' is not assignable to type 'number'. age: 'twenty', country: 'Chile', languages: ['english', 'spanish'], >;

You can also set an index signature to readonly if you need to prevent assignment to their indices.

Copied!
interface ReadonlyObj readonly [index: string]: any; > // 👇️ const p1: Person const p1: ReadonlyObj = name: 'Bobby Hadz', >; // ⛔️ Error: Index signature in type 'ReadonlyObj' // only permits reading. p1.name = 'James';

We set the string index’s type to readonly , so the type checker errors out if we try to write to a property that has a string key.

# Additional Resources

You can learn more about the related topics by checking out the following tutorials:

I wrote a book in which I share everything I know about how to become a better, more efficient programmer.

Источник

Читайте также:  Числа фибоначчи питон цикл
Оцените статью