TypeScript Fundamentals - Interfaces
Interfaces in TypeScript are used to name our types and define a contract with our code on what to expect from a variable, function, or any other language primitive. An Interface creates a new name for a type instance and can be extended. TypeScript checks the values passed and compares them to the declared interface to ensure that the shape of the values used match what is described in the interface.
If we have a function that takes an object, we can have an interface that ensures the object passed to the function has the properties we require in the function. Note that the passed object can have more properties besides the ones we have in the interface and that will be allowed by TypeScript(there are situations where we don’t want this to happen and we will see those shortly).
interface Account {
name: string;
amount: number;
accountNumber: string;
}
function showAccount(account: Account): void {
console.log(`${account.accountNumber}`);
console.log(`-----`);
console.log(`Name: ${account.name}: Cash Available: ${account.amount}`);
}
const myAccount = {
name: 'John Smith',
amount: 35000,
accountNumber: 'BankId-452938382892',
date: '2022-08-08',
};
showAccount(myAccount);
Even though we have an extra property date
, TS will not complain as long as the properties specified in the interface are present. Remember that since we are just printing values in the showAccount
function, we don’t have a returned value, so we used void
for the returned type.
The date
property can be made optional with a question mark(?) after the property name. With optional types we still have date
as part of the interface contract and even though it might not be provided in a passed object, we can still do checks for the date
property within the function knowing that it is part of the interface.
interface Account {
name: string;
amount: number;
accountNumber: string;
date?: string;
}
When describing an interface, we can have certain properties marked as readonly
and those properties will be set only when the object is being created. After that they can no longer be changed.
interface Car {
transmission: string;
readonly motor: string;
readonly tireCount: number;
}
let audi: Car = {
transmission: 'automatic',
motor: 'V6 Engine',
tireCount: 4,
};
audi.transmission = 'manual'; // OK
audi.tireCount = 6; // Error (readonly property)
Object literals undergo excess property checking when assigning them to other variables, or passing them as arguments to a function. This means that if the object literal has a property that doesn’t exist in the defined type, we get an error. So going back to the first case where we had an Account interface with three properties (name, account, accountNumber), if we passed an object literal to a function that expects an argument of type Account, and that object contains a property that is not in one of the defined three above, we will get an error.
interface Account {
name: string;
amount: number;
accountNumber: string;
}
function showAccount(account: Account): void {
console.log(`${account.accountNumber}`);
console.log(`-----`);
console.log(`Name: ${account.name}: Cash Available: ${account.amount}`);
}
showAccount({
name: 'John Smith',
amount: 35000,
accountNumber: 'BankId-452938382892',
date: '2022-08-08',
}); // Error, because date is not in the Account interface
There are ways to get past the excess property checks above.
The first way is to type assert the object literal to the Account type. The second way is to add a string index signature to the type to indicate that the interface can have any number of other properties.
//First method
showAccount({
name: 'John Smith',
amount: 35000,
accountNumber: 'BankId-452938382892',
date: '2022-08-08',
} as Account);
//Second method
interface Account {
name: string;
amount: number;
accountNumber: string;
[propName: string]: any;
}
Keep in mind that in the last two code snippets, we are passing an object literal to the showAccount
function. If we had saved the object to a variable and passed the variable to the function instead, we will not get any errors.
interface Account {
name: string;
amount: number;
accountNumber: string;
}
const myAccount = {
name: 'John Smith',
amount: 35000,
accountNumber: 'BankId-452938382892',
date: '2022-08-08',
};
showAccount(myAccount); // No errors since we are not using object literals.
Types for Functions
We can use an interface to describe the parameters and return types of functions just like we do variables. Then we can attach the type to the function name to check the signature.
interface HashFunc {
(text: string, toString: boolean, seed: number): string | number
}
const createHash: HashFunc = (str, toString, seed) {
let i, l, hVal = (seed === undefined) ? 0x811c9dc5 : seed
for (i = 0, l = str.length; i < l; i++) {
hVal ^= str.charCodeAt(i)
hVal += (hVal << 1) + (hVal << 4) + (hVal << 7) + (hVal << 8) + (hVal << 24)
}
if( toString ){
// Convert to 8 digit hex string
return ("0000000" + (hVal >>> 0).toString(16)).substr(-8);
}
return hVal >>> 0;
}
const hash: string | number = createHash('John Smith', true, 10)
console.log(hash) // "b4fa3b8c"
The createHash
function has its types and returned type inferred from the interface, so we don’t have to specify them when we write the function. Also we can see that based on the toString
parameter, the function can either return a number or string (union type).
Indexable Types
We can also define types for the index signature of types like Arrays and Tuples. This makes TypeScript aware of what kind of values can be passed in the index and also what will be returned from the index call.
interface TsArray {
[index: number]: string;
}
const teachers: TsArray = ['Mike', 'Thomas', 'Michael', 'John'];
const principal: string = teachers[2];
console.log(principal); // 'Michael'
Class Types
An interface can be used to describe methods and instance properties of a class. If the class implements that interface, it will be required to abide by the contract of the interface and implement the methods and properties of the interface in the class with the correct signatures.
interface CarInterface {
color: string;
make: string;
model: string;
start(key: number): void;
}
class Car implements CarInterface {
color: string;
make: string;
model: string;
constructor(color: string, make: string, model: string) {
this.color = color;
this.make = make;
this.model = model;
}
start(key: number) {
console.log(`Starting car with code number ${key}`);
}
}
With classes in TypeScript, there are two types, one for the instance side and one for the static side. The static side type defines the type for the constructor of the class while the instance side defines the methods and properties. Using a class expression, we can type both sides when defining a class.
interface CarInterface {
color: string;
make: string;
model: string;
start(key: number): void;
}
interface CarConstructor {
new (color: string, make: string, model: string);
}
const Car: CarConstructor = class Car implements CarInterface {
color: string;
make: string;
model: string;
//Parameter names don't have to match the interface names
constructor(clr: string, mke: string, mdl: string) {
this.color = clr;
this.make = mke;
this.model = mdl;
}
start(key: number) {
console.log(`Starting car with code number ${key}`);
}
};
Extending Interfaces
An interface can extend one or many other interfaces to aid with composable patterns. This allows us to reuse our interfaces in many different ways.
interface Limbs {
legCount: number;
hasHands: boolean;
}
interface Head {
eyeCount: number;
headShape: string;
}
interface Animal extends Limbs, Head {
name: string;
}
let human = {} as Animal;
human.legCount = 2;
human.hasHands = true;
human.eyeCount = 2;
human.headShape = 'Oval';
human.name = 'Homo Sapiens';