Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What does a TypeScript index signature actually mean?

I've been writing TypeScript for a while and am confused about what an index signature means.

For example, this code is legal:

function fn(obj: { [x: string]: number }) {
    let n: number = obj.something;
}

But this code, which does basically the same thing, isn't:

function fn(obj: { [x: string]: number }) {
    let p: { something: number } = obj;
}

Is this a bug? What's the intended meaning of this?

like image 587
Ryan Cavanaugh Avatar asked Oct 18 '19 21:10

Ryan Cavanaugh


People also ask

What is index signature in TypeScript?

Index signature is used to represent the type of object/dictionary when the values of the object are of consistent types. Syntax: { [key: KeyType] : ValueType } Assume that we have a theme object which allows us to configure the color properties that can be used across the application.

Is incompatible with index signature?

The error "Property is incompatible with index signature" occurs when a property is not compatible with the specified type of the index signature. To solve the error, change the type of the property or use a union to update the type in the index signature.

How do you define a key value pair in TypeScript?

Use an index signature to define a key-value pair in TypeScript, e.g. const employee: { [key: string]: string | number } = {} .

What is a mapped object type?

A mapped object type operates on a set of singleton types and produces a new object type where each of those singletons is turned into a property name. For example, this: type Foo = { [K in "hello" | "world"]: string }; would be equivalent to type Foo = { "hello": string; "world": string; };

When to use record<Keys type> vs index signature in typescript?

TypeScript has a utility type Record<Keys, Type> to annotate records, similar to the index signature. The big question is... when to use a Record<Keys, Type> and when an index signature? At first sight, they look quite similar! As you saw earlier, the index signature accepts only string, number or symbol as key type.

What is the value type of the index signature?

The index signature consists of the index name and its type in square brackets, followed by a colon and the value type: { [indexName: KeyType]: ValueType }. KeyType can be a string, number, or symbol, while ValueType can be any type.

How to define key type in typescript?

In your case, your keys can only be of type string. And your values can be whatever. The reason behind this is that, in typescript, you can define an object to have certain property : This create an type of object what MUST have both property to be valid. Because of that, there is not intuitive way to define key type.

Why do we use map/set types instead of index signatures?

Using Map/Set types over index signatures allows us to avoid quirks such as accessing Object.prototype values when accessing the keys of an object built using an index signature. What to do when you know the keys in your type but are not sure how many there will be?


Video Answer


1 Answers

You are right to be confused. Index signatures mean a few things, and they mean slightly different things depending where and how you ask.

First, index signatures imply that all declared properties in the type must have a compatible type.

interface NotLegal {
    // Error, 'string' isn't assignable to 'number'
    x: string;
    [key: string]: number;
}

This is a definitional aspect of index signatures -- that they describe an object with different property keys, but a consistent type across all those keys. This rule prevents an incorrect type from being observed when a property is accessed through an indirection:

function fn(obj: NotLegal) {
    // 'n' would have a 'string' value
    const n: number = obj[String.fromCharCode(120)];
}

Second, index signatures allow writing to any index with a compatible type.

interface NameMap {
    [name: string]: number;
}
function setAge(ageLookup: NameMap, name: string, age: number) {
    ageLookup[name] = age;
}

This is a key use case for index signatures: You have some set of keys and you want to store a value associated with the key.

Third, index signatures imply the existence of any property you specifically ask for:

interface NameMap {
    [name: string]: number;
}
function getMyAge(ageLookup: NameMap) {
    // Inferred return type is 'number'
    return ageLookup["RyanC"];
}

Because x["p"] and x.p have identical behavior in JavaScript, TypeScript treats them equivalently:

// Equivalent
function getMyAge(ageLookup: NameMap) {
    return ageLookup.RyanC;
}

This is consistent with how TypeScript views arrays, which is that array access is assumed to be in-bounds. It's also ergonomic for index signatures because, very commonly, you have a known set of keys available and don't need to do any additional checking:

interface NameMap {
    [name: string]: number;
}
function getAges(ageLookup: NameMap) {
    const ages = [];
    for (const k of Object.keys(ageLookup)) {
        ages.push(ageLookup[k]);
    }
    return ages;
}

However, index signatures don't imply that any arbitrary, unspecific property will be present when it comes to relating a type with an index signature to a type with declared properties:

interface Point {
    x: number;
    y: number;
}
interface NameMap {
    [name: string]: number;
}
const m: NameMap = {};
// Not OK, which is good, because p.x is undefined
const p: Point = m;

This sort of assignment is very unlikely to be correct in practice!

This is a distinguishing feature between { [k: string]: any } and any itself - you can read and write properties of any kind on an object with an index signature, but it can't be used in place of any type whatsoever like any can.

Each of these behaviors is individually very justifiable, but taken as a whole, some inconsistencies are observable.

For example, these two functions are identical in terms of their runtime behavior, but TypeScript only considers one of them to have been called incorrectly:

interface Point {
    x: number;
    y: number;
}
interface NameMap {
    [name: string]: number;
}

function A(x: NameMap) {
    console.log(x.y);
}

function B(x: Point) {
    console.log(x.y);
}
const m: NameMap = { };
A(m); // OK
B(m); // Error

Overall, when you write an index signature on a type, you're saying:

  • It's OK to read/write this object with an arbitrary key
  • When I read a specific property from this object through arbitrary key, it's always present
  • This object doesn't have literally every property name in existence for the purposes of type compatibility
like image 176
Ryan Cavanaugh Avatar answered Sep 19 '22 06:09

Ryan Cavanaugh