Skip to main content

As object or array type

Interface in TypeScript can be used to define a type and also to implement it in the class. The most scenarios can be summarized as following:

  • as object type definition
  • as array type definition
  • as function type definition
  • interface extends interface
  • class implements interface

In this section, we mainly focus on the object and array type definition using interface. As function type definition will be introduced in Function: Types section.

Interface likes contract which defines the structure of the object, array, function and also class. However, we need to note that the TypeScript compiler does not convert interface to JavaScript. It uses interface for type checking.

Interface as object type

Normal case

TypeScript uses an interface to ensure the proper structure of an object. The following interface Person defines a type of a variable:

interface Person {
name: string;
age: number;
}

In the above example, an interface Person includes two properties name and age. Let's define some variables using this interface type:

let p1: Person = {name: 'kimi', age: 20}; // OK
let p2: Person = {name: 'kimi', a: 20}; // Error: 'a' does not exist in type 'Person'.
let p3: Person = {name: 'kimi', age: '100'}; // Error: Type 'string' is not assignable to type 'number'.

Indexable types

Indexable types have an index signature that describes the types we can use to index into the object, along with the corresponding return types when indexing. Let’s take an example:

interface Person {
name: string;
age: number;
[index: string]: any;
}
let p1: Person = {name: 'kimi', age: 20, gender: 'male'}; // OK
let p2: Person = {name: 'kimi', age: 20, id: 888}; // OK

There are two types of supported index signatures: string and number. It is possible to support both types of indexers.

tip

Recommend to use string indexer in object and numeric indexer in array.

Please pay more attention to return type, see following examplea:

interface Person {
name: string; // Error: Property 'name' of type 'string' is not assignable to string index type 'number'.
age: number; // OK
[index: string]: number;
}
interface Person {
name: string; // OK
age: number; // Error: Property 'age' of type 'number' is not assignable to string index type 'string'.
[index: string]: string;
}

You can imagine the [index: string]: string as the types' "BOSS" in interface, if others match the "BOSS" pattern, the returned type must be compatible with "BOSS" otherwise will cause an error.

In addition, properties of different types are acceptable if the index signature is a union of the property types:

interface Person {
name: string; // OK
age: number; // OK
[index: string]: string | number;
}

Optional properties

Sometimes, we may declare an interface with excess properties but may not expect all objects to define all the given interface properties. We can have optional properties, marked with a ?:

interface Person {
name: string;
age: number;
gender?: string;
}

In such cases, objects of the interface may or may not define these properties:

let p1: Person = {name: 'kimi', age: 20}; // OK
let p2: Person = {name: 'kimi', age: 20, gender: 'male'}; // OK

Readonly properties

TypeScript provides a way to mark a property as read only. This means that once a property is assigned a value, it cannot be changed!

interface Person {
name: string;
readonly age: number;
}

let p1: Person = {name: 'kimi', age: 20};

p1.age = 18; // Error: Cannot assign to 'age' because it is a read-only property.

Interface as array type

An interface can also define the type of an array where you can define the type of index as well as values.

Indexable types

interface StringArray1 {
[index: number]: string;
}

let strArr: StringArray1 = [123]; // Error: Type 'number' is not assignable to type 'string'.

strArr[0] = 123; // Type '123' is not assignable to type 'string'.
strArr['0'] = 123; // OK

In the above example, we can find that if the type of index is number, we will only check the array literal and strArr[0], not includes strArr["1"].

interface StringArray2 {
[index: string]: string;
}

let strArr: StringArray2 = [123]; // Error: Type 'number' is not assignable to type 'string'.

strArr[0] = 123; // Error: Type '123' is not assignable to type 'string'.
strArr['0'] = 123; // Error: Type '123' is not assignable to type 'string'.
tip

Above, strArr[0] = 123 also causes an error because TS will convert numeric indexer to string indexer automatically. In a way, [index: string]: string is equivalent to [index: string | number]: string(FYI, the syntax is incorrect).

Readonly properties

At this time, you may wonder what's the difference between the array types in Basic Types and interface indexable types, they both can be used to define the type of array.

Besides the difference mentioned above, we can also use readonly before index which would like this:

interface ReadonlyStringArray {
readonly [index: number]: string;
}

let strArr: ReadonlyStringArray = ['abc'];
strArr[1] = 'def'; // Error!

Duck typing

We sometimes get some excess fields besides the contract with backend API. How will TypeScript handle this?

interface Person {
name: string;
age: number;
}

function handleResult(result: Person) {
console.log(result.name, result.age);
}

const result = {name: 'Kimi', age: 20, gender: 'male'}; // OK

handleResult(result);

In the above example, we can find that there is no error in the end. Since TypeScript uses a kind of type checking called "duck typing" or "structural subtyping".

If it walks like a duck and it quacks like a duck, then it must be a duck.

In other words, don’t check whether it IS-a duck, check whether it QUACKS-like-a duck, WALKS-like-a duck, etc.

Thus, in TypeScript we only need to pass in the object that meets the necessary conditions of the interface. It will pass the type checking even if the excess fields are passed in.

caution

It will cause an error if we pass in object literal.

interface Person {
name: string;
age: number;
}

function handleResult(result: Person) {
console.log(result.name, result.age);
}

handleResult({name: 'Kimi', age: 20, gender: 'male'}); // Error: Object literal may only specify known properties, and 'gender' does not exist in type 'Person'.

Besides the above method which pass in object variable to avoid causing error, we can also use type assertion would look like:

handleResult({name: 'Kimi', age: 20, gender: 'male'} as Result); // OK

We can also use the indexable types and it's more flexible. Let's take an example:

interface Person {
name: string;
age: number;
[index: string]: any;
}

// ...

handleResult({name: 'Kimi', age: 20, gender: 'male'}); // OK

References

  1. Tutorials Teacher: TypeScript Interface
  2. TypeScript in action, By Liang Xiao
  3. Wikipedia: Duck typing
  4. TypeScript Official Docs: Interfaces