The big advantage of TypeScript over plain JavaScript is that it extends the features of JavaScript by adding type safety to our program’s objects. It does this by checking the shape of the values that objects can take on. Checking the shape is called duck typing or structural typing. It’s very useful for defining contracts within our code in TypeScript programs. In this article, we’ll look at how to define a TypeScript interface and add required or optional properties to it.
Defining Interfaces
To define a basic interface, we use the interface
keyword in TypeScript. This keyword is exclusive to TypeScript and it’s not available in JavaScript. We can define a TypeScript interface like in the code below:
interface Person {
name: string
}
In the code above, if a variable or parameter has been designated with this interface, then all objects with the type using it will have the name
property. Object literals assigned to a variable with the type cannot have any other property. Arguments that are passed in as parameters that are of this type also can only have this property. For example, the following code will be compiled successfully and run with the TypeScript compiler:
interface Person{
name: string
}
const greet = (person: Person) => {
console.log(`Hello, ${person.name}`);
}
greet({ name: 'Joe' });
The code above will compile and run since it only has the name
property and the value of it is a string exactly like it’s specified in the Person
interface. However, the following code wouldn’t be compiled by the TypeScript compiler and throw an error:
interface Person {
name: string
}
const greet = (person: Person) => {
console.log(`Hello, ${person.name}`);
}
greet({ name: 'Joe', foo: 'abc' });
This is because we specified that the type of the person
parameter in the greet
function has the type Person
. The object passed in as the argument can only have the name
property and that the value of it can only be a string. We can’t assign an object literal with different properties than what’s listed in the interface if a variable has been designated by the type of the interface:
const p: Person = { name: 'Joe', foo: 'abc' };
The code above also wouldn’t compile since the object literal has an extra property in it that is not listed in the Person
interface. In editors that support TypeScript like Visual Studio Code, we would get the error message “Type ‘{ name: string; foo: string; }’ is not assignable to type ‘Person’. Object literal may only specify known properties, and ‘foo’ does not exist in type ‘Person’.”
With interfaces, when designating the types of variables, we get auto-completion of our code when we write object literals.
Optional Properties
TypeScript interfaces can have optional properties. This makes interfaces much more flexible than just adding required properties to them. We can designate a property as optional with the question mark ?
after the property name. For example, we can write the following code to define an interface with an optional property:
interface Person {
name: string,
age?: number
}
In the code above, age
is an optional property since we put a question mark after the property name. We can use it as the following code:
interface Person {
name: string,
age?: number
}
const greet = (person: Person) => {
console.log(`Hello, ${person.name}. ${person.age ? `You're ${person.age} years old.` : ''}`);
}
greet({ name: 'Joe', age: 10 });
In the object we passed into the greet
function into the code above, we passed in the age
property and a number for its value. Then we used it inside our code by checking if it’s defined first and then we add additional text to the main string if it is. We can also omit optional properties like in the following code:
interface Person {
name: string,
age?: number
}
const greet = (person: Person) => {
console.log(`Hello, ${person.name}. ${person.age ? `You're ${person.age} years old.` : ''}`);
}
greet({ name: 'Joe' });
The code above would still run since the age
property has been designated as being optional. The rules outlined by the interface still apply if we assign an object literal to it. For example, if we write:
interface Person {
name: string,
age?: number
}
const greet = (person: Person) => {
console.log(`Hello, ${person.name}. ${person.age ? `You're ${person.age} years old.` : ''}`);
}
const person: Person = { name: 'Joe' };
greet(person);
This would still work since we stick to the property descriptions defined in the Person
interface. If we add the age
property with a number property it would still compile and run:
interface Person {
name: string,
age?: number
}
const greet = (person: Person) => {
console.log(`Hello, ${person.name}. ${person.age ? `You're ${person.age} years old.` : ''}`);
}
const person: Person = { name: 'Joe', age: 20 }
greet(person);
Optional properties are useful in that we can define properties that can possibly be used while preventing the use of properties that aren’t part of the interface. This prevents bugs that arise because of typos in our code.
Readonly Properties
To make properties that are non-modifiable after the object is first created, we can use the readonly
keyword in front of a property to designate that the property can only be written once when the object is being created and not any time after. For example, we can write the following code:
interface Person {
readonly name: string,
age?: number
}
const greet = (person: Person) => {
console.log(`Hello, ${person.name}. ${person.age ? `You're ${person.age} years old.` : ''}`);
}
const person: Person = { name: 'Joe', age: 20 };
greet(person);
We added the readonly
keyword in front of the name
property so that it can only be changed once and only once. So if we try to assign something new to the name
property, the TypeScript compiler wouldn’t compile and code and the code wouldn’t run:
interface Person{
readonly name: string,
age?: number
}
const greet = (person: Person) => {
console.log(`Hello, ${person.name}. ${person.age ? `You're ${person.age} years old.` : ''}`);
}
let person: Person = { name: 'Joe', age: 20 };
person.name = 'Jane';
greet(person);
The code above will get us the error message “ Cannot assign to ‘name’ because it is a read-only property” when we try to compile it or in text editors that support TypeScript. However, we can reassign the whole object to a different value like in the following code:
interface Person {
readonly name: string,
age?: number
}
const greet = (person: Person) => {
console.log(`Hello, ${person.name}. ${person.age ? `You're ${person.age} years old.` : ''}`);
}
let person: Person = { name: 'Joe', age: 20 };
person = { name: 'Jane', age: 20 };
greet(person);
Then instead of logging ‘Hello, Joe. You’re 20 years old.’, we get ‘Hello, Jane. You’re 20 years old.’
If we want to designate an array as read-only, that is, only be changed when it’s created, we can use the ReadonlyArray
type that’s the same as Array
, but all the mutating methods are removed from it so that we can’t accidentally change anything in the array. For example, we can use it as in the following code:
let readOnlyArray: ReadonlyArray<number> = [1,2,3];
Then if we try to write the following code, the TypeScript compiler would give errors:
readOnlyArray[0] = 12;
readOnlyArray.push(5);
readOnlyArray.length = 100;
Then we get the following errors:
Index signature in type 'readonly number[]' only permits reading.(2542)Property 'push' does not exist on type 'readonly number[]'.(2339)Cannot assign to 'length' because it is a read-only property.(2540)
If we want to convert a ReadonlyArray
back to a writable array, we can use the type assertion as
operator to convert it back to a regular array:
let arr = readOnlyArray as number[];
Conclusion
TypeScript interfaces are very handy for defining contracts within our code. We can use it to designate types of variables and function parameters. They let us know what properties a variable can take on, and whether they’re required, optional, or read-only. We can define interfaces with the interfaces
keyword, optional properties with the question mark after a variable name, and the readonly
keyword for read-only properties.
Top comments (0)