Categories
TypeScript

Introduction to TypeScript Interfaces — Object Literals and Function Types

Spread the love

The big advantage of TypeScript over plain JavaScript is that it extends the features of JavaScript by adding functionality that ensures the type safety of our program’s objects. It does this by checking the shape of the values that objects take on.

Checking the shape is called duck typing or structural typing. Interfaces are one way to fill the role naming data types in TypeScript. It’s very useful for defining contracts within our code in TypeScript programs. In the last article, we looked at how to define a TypeScript interface and adding required and optional properties to it. In this article, we’ll continue from the previous article and look at other properties of TypeScript interfaces.

Excess Property Checks

Object properties get extra checks when they’re being assigned to a variable with the type designated by an interface. This also applies to object literals that we pass into functions as arguments. For example, the following code wouldn’t be compiled by the TypeScript compiler and give us an error:

interface Person{
  name: string
}

const greet = (person: Person) => {
  console.log(`Hello, ${person.name}`);
}

greet({ name: 'Joe', foo: 'abc' });

The excess property check done by the TypeScript compiler will reject the code since we have an extra foo property that isn’t defined in the Person interface, so add it in the object in the parameter would fail because of TypeScript’s excess property checks for object literals. Assigning the same object literal to a variable will also fail. For example, if we have the following code:

interface Person{
  name: string
}
const greet = (person: Person) => {
  console.log(`Hello, ${person.name}`);
}
const person: Person = { name: 'Joe', foo: 'abc' };
greet(person);

We would get the error “Type ‘{ name: string; foo: string; }’ is not assignable to type ‘Person’. Object literal may only specify known properties, and ‘foo’” if we try to compile the code with the TypsScript compiler or look at the code at a text editor that supports TypeScript. However, we can use the type assertion operator as to designate the type of the object literal as we like it. So if we’re sure that the object literal if of the type Person even though it has a foo property in it, we can write the following code:

interface Person{
  name: string
}
const greet = (person: Person) => {
  console.log(`Hello, ${person.name}`);
}
const person: Person = { name: 'Joe', foo: 'abc' } as Person;
greet(person);

With the code above, the TypeScript compiler won’t complain of any issues. It’ll just assumes that the object literal is of type Person even though it has a foo property. However, we do have some properties that are dynamic or may only sometimes appear, we can also add a dynamic property to our TypeScript interfaces like in the following code:

interface Person{
  name: string,
  [prop: string]: any
}

const greet = (person: Person) => {
  console.log(`Hello, ${person.name}. ${person.age ? `You're ${person.age} years old.` : ''}`);
}

const person: Person = { name: 'Jane', age: 20 };
greet(person);

In the code above, we added:

[prop: string]: any

to our Person interface. The line above means that the type Person can have any other property other than name . The property name is a string, which is the case for the dynamic property names in JavaScript, and these dynamic properties can take on any value since specified the any type for the dynamic property. As we can see, we have the following line:

const person: Person = { name: 'Jane', age: 20 };

where our object literal has the age property but it’s not explicitly defined in our interface definition. This is because we have the dynamic property after the name property. The [prop: string] is called the index signature.

We can also get around the excess property check for object literals by assigning a variable to another variable. For example, if we have the following code:

interface Person{
  name: string
}

const greet = (person: Person) => {
  console.log(`Hello, ${person.name}. ${person.age ? `You're ${person.age} years old.` : ''}`);
}

const person: Person = { name: 'Jane', age: 20 };
greet(person);

which wouldn’t compile and run because of the excess property check, we can get around it by assigning the person constant to a new variable or constant that doesn’t have a type designated to it like we do below:

interface Person{
  name: string
}

const greet = (person: Person) => {
  console.log(`Hello, ${person.name}`);
}

const person = { name: 'Jane', age: 20 };
greet(person);

The person constant doesn’t have a type designated to it so the excess property check for object literals won’t be run.

The excess property check is recommended to be enforced for simple objects like the ones we have above. For more complex, dynamic objects, we can use the ways we outline above to get around the checks to get the code running. However, do be aware that most excess property errors are actually typos in our code, so they’re legitimate bugs that should be fixed.

Photo by Max Baskakov on Unsplash

Function Types

With TypeScript interfaces, we can also define the signature of functions by designating the data type for each parameter and the return type of the function. This prevents us from passing in parameters that have the wrong data type or forgetting to pass in arguments into our function calls, and also ensures that our function always have the same return type and we won’t be returning things that we don’t expect in our code.

We can define a interface for designating the parameter and return data types of our function, and the function signature like we do in the code below:

interface GreetFn{
  (name: string, age: number): string
}

const greet: GreetFn = (name: string, age: number) => {
  return `Hello, ${name}. You're ${age} years old`;
}

console.log(greet('Jane', 20));

The code above has the function greet that follows the function signature defined on the left side of the colon in the GreetFn interface and the return data type on the right side of the interface, so the code will run and produce output from the console.log statement in the last line. We should get ‘Hello, Jane. You’re 20 years old’. If we designate our greet function with the type GreetFn but our function signature or return type stray away from the ones designated in the GreetFn interface then we’ll get errors. For example, if we have:

interface GreetFn{
  (name: string, age: number): string
}
const greet: GreetFn = (name: string, age: number, foo: any) => {
  return `Hello, ${name}. You're ${age} years old`;
}
console.log(greet('Jane', 20));

Then we’ll get the error message “Type ‘(name: string, age: number, foo: any) => string’ is not assignable to type ‘GreetFn’.(2322)“ since our parameter list doesn’t match the signature listed in the interface. Likewise, if our function’s return type doesn’t match the one we defined in the interface, we’ll also get an error. For example if we have the following code:

interface GreetFn{
  (name: string, age: number): string
}
const greet: GreetFn = (name: string, age: number) => {
  return 0;
}
console.log(greet('Jane', 20));

We’ll get the error “Type ‘(name: string, age: number) => number’ is not assignable to type ‘GreetFn’. Type ‘number’ is not assignable to type ‘string’.” This means that the greet function must return a string since we specified that the type of the greet function is GreetFn .

Function parameters are checked one at a time, so the TypeScript compiler infers the position of the type of a parameter by its position even though no types are designated by us when we define our function. For example, the following will still work even though we didn’t specified the type of our parameters explicitly:

interface GreetFn{
  (name: string, age: number): string
}
const greet: GreetFn = (name, age) => {
  return `Hello, ${name}. You're ${age} years old`;
}
console.log(greet('Jane', 20));

If we pass in something with the wrong data type according to the interface we defined like in the code below, we’ll get an error:

interface GreetFn{
  (name: string, age: number): string
}
const greet: GreetFn = (name, age) => {
  return `Hello, ${name}. You're ${age} years old`;
}
console.log(greet('Jane', ''));

When we try to compile the code above, we’ll get the error “Argument of type ‘“”’ is not assignable to parameter of type ‘number’.(2345)“. This means that TypeScript is smart enough to infer the type by its position. Type inference is also done for the return type, so if we write the following code:

interface GreetFn{
  (name: string, age: number): string
}
const greet: GreetFn = (name, age) => {
  return 0;
}
console.log(greet('Jane', 20));

Then we’ll get the error “Type ‘(name: string, age: number) => number’ is not assignable to type ‘GreetFn’. Type ‘number’ is not assignable to type ‘string’.(2322)” so the code won’t compile.

Excess property checks for object literals are useful since it’s much harder for us to add wrong properties or typos into our code when we’re assigning object literals or passing them in as arguments of functions. We can get around it with type assertion or assigning to a variable with different types or no types. We can also define interfaces for functions to define the expected parameters for a function and also the expected return type for them.

By John Au-Yeung

Web developer specializing in React, Vue, and front end development.

Leave a Reply

Your email address will not be published. Required fields are marked *