Skip to main content

interfaces-optional-properties-and-function-types

7. Interfaces: Optional Properties and Function Types

  • Further discuss how interfaces can have optional properties and can also be used to type check functions.

Certainly, understanding interfaces—especially their optional properties and how they relate to function types—is pivotal for advanced TypeScript development. Here are 20 key points to focus on:

Basics of Interfaces:

Interfaces are like contracts for your code. They are a way to tell TypeScript what the shape of an object should be. By defining an interface, you can specify which properties and methods a class or object should have, and TypeScript will enforce that contract throughout your code.

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

let employee: Person = {
name: "John Doe",
age: 30
};

Defining Optional Properties:

You can make some properties optional in an interface, meaning that objects of that interface type might or might not have those properties. It's a way to say, "This property might be there, but it doesn't have to be."

interface Person {
name: string;
age?: number; // age is optional
}

let student: Person = {
name: "Jane Doe"
// It's okay to omit the age property
};

Understanding readonly Properties:

readonly properties must be set when you first create an object. They can't be changed later. It's like saying, "This can be set once, and then it's set in stone."

interface Point {
readonly x: number;
readonly y: number;
}

let p1: Point = { x: 10, y: 20 };
// p1.x = 5; // Error! Cannot assign to 'x' because it is a read-only property.

Excess Property Checking:

When using interfaces, TypeScript checks to make sure there aren't any unexpected properties. This helps catch typos and unnecessary properties at compile time.

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

let person: Person = {
name: "Alice",
// age: 25,
// occupation: "Developer" // Error! Object literal may only specify known properties, and 'occupation' does not exist in type 'Person'.
};

Indexable Types:

Interfaces can be used to describe types that allow for indexing, like arrays or objects with dynamic property names.

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

let myArray: StringArray;
myArray = ["Bob", "Fred"];

Function Types within Interfaces:

Interfaces can define function types by providing a signature for the function. This lets you ensure that any function that matches the interface has the correct form.

interface SearchFunc {
(source: string, subString: string): boolean;
}

let mySearch: SearchFunc;
mySearch = function(source: string, subString: string) {
return source.search(subString) > -1;
};

Implementing an Interface:

When a class implements an interface, it is signing a contract to follow the structure defined by the interface. This helps maintain consistent shapes across different parts of your application.

interface ClockInterface {
currentTime: Date;
}

class Clock implements ClockInterface {
currentTime: Date = new Date();
// Class 'Clock' correctly implements interface 'ClockInterface'.
}
  1. Extending Interfaces:

    The extends keyword allows an interface to inherit properties from other interfaces. This promotes reusability and composability.

interface Named {
name: string;
}

interface Aged extends Named {
age: number;
}

let person: Aged = { name: 'Alice', age: 25 };
  1. Intersection with Interfaces:

    Interfaces can be intersected using the & operator. This allows you to combine multiple interfaces into one.

interface Named {
name: string;
}

interface Loggable {
log(): void;
}

type NamedAndLoggable = Named & Loggable;

let person: NamedAndLoggable = {
name: 'Alice',
log: () => console.log('Logging!')
};
  1. Optional Function Parameters:

    Just like optional properties, interfaces can specify optional parameters for functions using the ? symbol.

interface Greeter {
greet(name: string, greeting?: string): void;
}

class GreeterClass implements Greeter {
greet(name: string, greeting: string = 'Hello') {
console.log(`${greeting}, ${name}`);
}
}

const greeter = new GreeterClass();
greeter.greet('Alice'); // Will use the default greeting 'Hello'
  1. Function Overloads in Interfaces:

    Interfaces support function overloading, letting you specify multiple function signatures within the same interface.

interface Document {
createElement(tagName: any): Element;
createElement(tagName: 'div'): HTMLDivElement;
createElement(tagName: 'span'): HTMLSpanElement;
}

class DocumentClass implements Document {
createElement(tagName: any): Element {
// Implementation goes here
}
}
  1. Generic Interfaces:

    Interfaces can be generic, allowing them to be used with a variety of types. This promotes code reusability.

interface Repository<T> {
findById(id: number): T;
save(entity: T): void;
}

class User {}

class UserRepository implements Repository<User> {
findById(id: number): User {
// Implementation goes here
return new User();
}

save(user: User): void {
// Implementation goes here
}
}
  1. Utility Types with Interfaces:

    Built-in utility types like Partial and Readonly can be applied to interfaces to create modified versions of them.

interface Todo {
title: string;
description: string;
}

type PartialTodo = Partial<Todo>; // All properties become optional
type ReadonlyTodo = Readonly<Todo>; // All properties become readonly

let todo: PartialTodo = { title: 'Write TypeScript' }; // Description is optional
let fixedTodo: ReadonlyTodo = { title: 'Fix bug', description: 'Fix the layout bug on the homepage' };
// fixedTodo.title = 'Another title'; // Error: Cannot assign to 'title' because it is a read-only property

Type Assertion with Interfaces:

Type assertion is like telling TypeScript, "Trust me, I know what I'm doing." It's a way to say, "This is the type of the object." You can use this when TypeScript can't figure out the type, but you know better because of information TypeScript doesn't have.

interface Cat {
meow: number;
}

let pet = {} as Cat;
pet.meow = 3;

Discriminated Unions with Interfaces:

Discriminated unions use a common property to determine which type something could be. It's like a detective following clues to figure out what type of object they're dealing with.

interface Circle {
kind: "circle";
radius: number;
}

interface Square {
kind: "square";
sideLength: number;
}

type Shape = Circle | Square;

function handleShape(shape: Shape) {
if (shape.kind === "circle") {
// TypeScript knows this is Circle
} else {
// TypeScript knows this is Square
}
}

Interface Merging:

Interface merging is when TypeScript takes multiple interface definitions with the same name and combines them into one. It's like you're saying, "Here's some more information about this thing."

interface Box {
height: number;
}

interface Box {
width: number;
}

let box: Box = { height: 5, width: 6 };

Interfaces vs. Types:

interfaces and types can both be used to define the shape of objects, but they work a bit differently. Interfaces are more extendable and can be merged, but types can handle more complex type definitions.

// Using an interface
interface Point {
x: number;
y: number;
}

// Using a type
type PointType = {
x: number;
y: number;
};

Type Compatibility:

TypeScript uses the shape of values for checking compatibility, which means it focuses on the structure and capabilities of types. Interfaces provide the shape that TypeScript uses for these checks.

interface Named {
name: string;
}

let x: Named;
// y's inferred type is { name: string; location: string; }
let y = { name: "Alice", location: "Seattle" };
x = y; // OK

Callable and Constructable Interfaces:

Interfaces in TypeScript can describe functions and constructors, not just object structures. This lets you enforce specific signatures for callable or constructable elements.

interface CallMeWithNewToGetString {
new (): string;
}

// Usage
// let myCallableObject: CallMeWithNewToGetString;
// let str = new myCallableObject();

Best Practices for Interfaces:

When using interfaces, it's good to stick to certain best practices like giving meaningful names, keeping them focused on one thing, and organizing them well. This makes your code easier to understand and maintain.

// Good Interface
interface User {
id: string;
name: string;
}

// Bad Interface (violates single responsibility)
interface UserWithSettings {
id: string;
name: string;
settings: {
theme: string;
notifications: boolean;
};
// Better to create a separate interface for Settings
}