Execute Program

Everyday TypeScript: Index Signatures

Welcome to the Index Signatures lesson!

This lesson is shown as static text below. However, it's designed to be used interactively. Click the button below to start!

  • We've seen object types that include specific keys:

  • >
    type User = {name: string};
    const amir: User = {name: 'Amir'};
    amir.name;
    Result:
    'Amir'Pass Icon
  • What about objects where we don't know the keys in advance? For example, we might want a LoginCounts object type that maps users' names to how many times they've logged in. We can't explicitly write out the keys for that object type because we don't know all of the users' names at compile time.

  • Fortunately, TypeScript has a way to express this type. The syntax is based on ES2015's "computed keys" feature, so we'll review that first.

  • When creating an object, we can write the keys as unquoted words, like Amir in this example:

  • >
    const loginCounts = {Amir: 5};
    loginCounts.Amir;
    Result:
    5Pass Icon
  • We can also create objects with computed keys. To do that, we wrap the key in square brackets, like {[someVariable]: 1}:

  • >
    const name = 'Amir';
    const loginCounts = {[name]: 5};
    loginCounts.Amir;
    Result:
  • The [square brackets] can contain any expression, not just simple variables. For example, we can use string concatenation to build the key dynamically:

  • >
    const loginCounts = {['Be' + 'tty']: 7};
    loginCounts.Betty;
    Result:
    7Pass Icon
  • TypeScript has a similar syntax for defining object types with arbitrary keys. Here's a LoginCounts type for objects that map strings to numbers:

  • >
    type LoginCounts = {[userName: string]: number};
    const loginCounts: LoginCounts = {
    Amir: 5,
    Betty: 7,
    };
    loginCounts.Betty;
    Result:
    7Pass Icon
  • Any object that violates the type will cause a type error. (You can indicate type errors with type error.)

  • >
    type LoginCounts = {[userName: string]: number};
    const loginCounts: LoginCounts = {
    Amir: 5,
    Betty: 'seven',
    };
    Result:
    type error: Type 'string' is not assignable to type 'number'.Pass Icon
  • Objects with these types are still regular objects. The type just describes the type of the keys and the values.

  • >
    type LoginCounts = {[userName: string]: number};
    const loginCounts: LoginCounts = {
    Amir: 5,
    Betty: 7,
    };
    loginCounts;
    Result:
  • In our type above, we named the key userName. TypeScript doesn't care whether we name it userName or key or s or anything else. But it's a good idea to use a name that indicates intent. We intend for this object's keys to be users' names, and userName communicates that to the next person who reads our type.

  • This kind of type is called an "index signature". "Index" because it's defining the indexes (that is, the keys) of an object. "Signature" because it defines the object's overall signature, in the same way that the type (n: number) => string defines a function's signature.

  • Here's a code problem:

    Write the index signature for an object that maps a person's name (a string) to a city (a string or undefined).

    type Cities = {[userName: string]: string | undefined};
    const cities: Cities = {
    Amir: 'Paris',
    Betty: 'Nassau',
    Cindy: undefined,
    };
    cities;
    Goal:
    {Amir: 'Paris', Betty: 'Nassau', Cindy: undefined}
    Yours:
    {Amir: 'Paris', Betty: 'Nassau', Cindy: undefined}Pass Icon
  • Index signatures' key types must be strings or numbers. When they're numbers, they define an array type. Here's an array type written normally:

  • >
    const strings: string[] = ['a', 'b', 'c'];
    strings[0];
    Result:
    'a'Pass Icon
  • Here's the same array type written using an index signature. We wouldn't normally do this, but it nicely demonstrates that JavaScript arrays are just objects with number keys.

  • >
    const strings: {[index: number]: string} = ['a', 'b', 'c'];
    strings[0];
    Result:
    'a'Pass Icon
  • It's possible to mix an index signature with explicit definitions of certain fields. When we do that, the other fields must match the index signature. You can think of the index signature as being the overall type for the object. Any individual fields must conform to that overall type; otherwise it's a type error.

  • >
    type LoginCounts = {
    [userName: string]: number
    Amir: number
    };
    const loginCounts: LoginCounts = {
    Amir: 5,
    Betty: 7,
    };
    loginCounts;
    Result:
    {Amir: 5, Betty: 7}Pass Icon
  • >
    type LoginCounts = {
    [userName: string]: number
    Amir: string
    };
    const loginCounts: LoginCounts = {
    Amir: 'five',
    Betty: 7,
    };
    loginCounts;
    Result:
    type error: Property 'Amir' of type 'string' is not assignable to 'string' index type 'number'.Pass Icon
  • >
    type LoginCounts = {
    [userName: string]: number
    Amir: number
    };
    const loginCounts: LoginCounts = {
    Betty: 7,
    };
    loginCounts;
    Result:
    type error: Property 'Amir' is missing in type '{ Betty: number; }' but required in type 'LoginCounts'.Pass Icon
  • Finally, a warning about index signatures. Object access can always return an undefined, even if it's not in the index signature type.

  • Consider the index signature {[key: string]: string}. It says "every key is a string that maps to a string value". But that's impossible because there's an infinite number of possible strings that could be keys!

  • If we use that index signature, then access a key that doesn't exist, we'll get undefined, even though there's no undefined in the type! The type system will think that we got a string even though it's actually an undefined.

  • >
    type UserEmails = {[name: string]: string};
    const userEmails: UserEmails = {Amir: 'amir@example.com'};
    userEmails.Amir;
    Result:
    'amir@example.com'Pass Icon
  • >
    type UserEmails = {[name: string]: string};
    const userEmails: UserEmails = {Amir: 'amir@example.com'};
    userEmails.Betty;
    Result:
    undefinedPass Icon
  • This code is wrong, but unfortunately it's allowed by the compiler.

  • >
    type UserEmails = {[name: string]: string};
    const userEmails: UserEmails = {Amir: 'amir@example.com'};
    const email: string = userEmails.Betty;
    email;
    Result:
    undefinedPass Icon
  • This is one of the places where TypeScript's type system lets us do things that are wrong and dangerous. Fortunately, we can change this behavior with the --noUncheckedIndexedAccess compiler option. With that option enabled, userEmails.Betty has the type string | undefined, accurately reflecting the fact that Betty may not be present in the object. If you combine the --noUncheckedIndexedAccess compiler option with the --strictNullChecks option, you'll get increased type safety at the cost of writing a little more code.