阿里云主机折上折
  • 微信号
Current Site:Index > Comparison between type aliases and interfaces

Comparison between type aliases and interfaces

Author:Chuan Chen 阅读数:24762人阅读 分类: TypeScript

Basic Concepts of Type Aliases and Interfaces

TypeScript provides two main ways to define custom types: type aliases and interfaces. Both can be used to describe object shapes, function signatures, etc., but they differ in usage scenarios and features.

Type aliases are created using the type keyword:

type User = {
  name: string;
  age: number;
};

Interfaces are declared using the interface keyword:

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

Syntax Differences

Type aliases can represent any type, including primitive types, union types, tuples, etc.:

type ID = string | number;
type Point = [number, number];
type Callback = (data: string) => void;

Interfaces focus on describing object shapes:

interface Point {
  x: number;
  y: number;
}
interface Callback {
  (data: string): void;
}

Extension Methods

Interfaces use extends for inheritance:

interface Animal {
  name: string;
}
interface Dog extends Animal {
  breed: string;
}

Type aliases use intersection types & for composition:

type Animal = {
  name: string;
};
type Dog = Animal & {
  breed: string;
};

Declaration Merging

Interfaces support declaration merging—identically named interfaces automatically merge:

interface User {
  name: string;
}
interface User {
  age: number;
}
// The final User includes both name and age

Type aliases cannot be redeclared and will throw an error:

type User = { name: string };
type User = { age: number }; // Error

Implementation and Extension

Classes can implement (implements) either interfaces or type aliases:

interface ClockInterface {
  currentTime: Date;
}
class Clock implements ClockInterface {
  currentTime: Date = new Date();
}

type ClockType = {
  currentTime: Date;
};
class DigitalClock implements ClockType {
  currentTime: Date = new Date();
}

Representing Complex Types

Type aliases are better suited for representing complex types:

type Tree<T> = {
  value: T;
  left?: Tree<T>;
  right?: Tree<T>;
};

Interfaces can also represent recursive structures but with slightly more verbose syntax:

interface Tree<T> {
  value: T;
  left?: Tree<T>;
  right?: Tree<T>;
}

Tuples and Union Types

Type aliases handle tuples and union types more intuitively:

type StringOrNumber = string | number;
type Coordinates = [number, number];

Interfaces require special syntax to represent these types:

interface StringOrNumber {
  /* Cannot directly represent union types */
}
interface Coordinates extends Array<number> {
  0: number;
  1: number;
  length: 2;
}

Performance Considerations

In large codebases, interfaces may perform better than type aliases. The TypeScript compiler has more optimizations for interfaces, especially when checking type compatibility.

Usage Scenario Recommendations

  1. Choose interfaces when declaration merging is needed.
  2. Choose type aliases for representing union types, tuples, or other non-object types.
  3. Both can be used for class implementation, but interfaces align better with traditional OOP thinking.
  4. Library type definitions typically prefer interfaces for easier extension by users.

Choices in Real Projects

In React projects, Props and State type definitions are often handled like this:

// Using interfaces
interface Props {
  title: string;
  onClick: () => void;
}

// Using type aliases
type State = {
  count: number;
  isLoading: boolean;
};

In Redux, action types often use type aliases:

type Action =
  | { type: 'INCREMENT' }
  | { type: 'DECREMENT' }
  | { type: 'SET_COUNT'; payload: number };

Type Inference Differences

Type aliases and interfaces may behave differently in certain type inference scenarios:

type Named = { name: string };
interface Named {
  name: string;
}

function getName(n: Named) {
  return n.name;
}

// Literal objects undergo extra property checks
getName({ name: 'Alice', age: 30 }); // Error
const obj = { name: 'Bob', age: 25 };
getName(obj); // Correct

Utility Type Operations

Type aliases work better with utility types:

type PartialUser = Partial<User>;
type ReadonlyUser = Readonly<User>;
type UserWithoutAge = Omit<User, 'age'>;

Interfaces can also work with these utility types, but the result becomes a type alias:

type PartialUser = Partial<IUser>; // Result is a type alias

Function Type Definitions

Both approaches can define function types:

// Type alias approach
type Greet = (name: string) => string;

// Interface approach
interface Greet {
  (name: string): string;
}

Function types with properties:

type Counter = {
  (start: number): string;
  interval: number;
  reset(): void;
};

interface Counter {
  (start: number): string;
  interval: number;
  reset(): void;
}

Index Signatures

Both support index signatures:

type StringArray = {
  [index: number]: string;
};

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

Mapped Types

Type aliases are better for creating mapped types:

type Keys = 'name' | 'age';
type User = {
  [K in Keys]: string;
};

Conditional Types

Type aliases support conditional types; interfaces do not:

type IsString<T> = T extends string ? true : false;
type A = IsString<'hello'>; // true
type B = IsString<123>; // false

Default Exports

When exporting modules, both behave the same:

export type { User };
export interface User {}

Type Queries

The typeof operator behaves the same for both:

const user = { name: 'Alice', age: 30 };
type UserType = typeof user; // { name: string; age: number }

Type Guards

Type aliases and interfaces behave the same in type guards:

interface Bird {
  fly(): void;
}
type Fish = {
  swim(): void;
};

function isFish(pet: Fish | Bird): pet is Fish {
  return (pet as Fish).swim !== undefined;
}

Generic Constraints

Both can be used as generic constraints:

interface Lengthwise {
  length: number;
}
function loggingIdentity<T extends Lengthwise>(arg: T): T {
  console.log(arg.length);
  return arg;
}

type Lengthwise = {
  length: number;
};
function loggingIdentity<T extends Lengthwise>(arg: T): T {
  console.log(arg.length);
  return arg;
}

Recursive Types

Type aliases offer more concise syntax for recursive type definitions:

type Json =
  | string
  | number
  | boolean
  | null
  | Json[]
  | { [key: string]: Json };

interface Json {
  // Cannot directly represent such complex recursive union types
}

Relationship with Classes

Interfaces can describe both class instances and static parts:

interface ClockConstructor {
  new (hour: number, minute: number): ClockInterface;
}
interface ClockInterface {
  tick(): void;
}

Type aliases can also represent similar structures:

type ClockConstructor = new (hour: number, minute: number) => ClockInterface;
type ClockInterface = {
  tick(): void;
};

Type Extraction

Type aliases are more flexible for extracting parts of existing types:

type User = {
  id: number;
  name: string;
  address: {
    street: string;
    city: string;
  };
};
type Address = User['address'];

String Literal Types

Type aliases can conveniently define string literal types:

type Direction = 'north' | 'east' | 'south' | 'west';

Interfaces cannot directly represent such types and require additional definitions:

interface Direction {
  value: 'north' | 'east' | 'south' | 'west';
}

Performance Optimization

For large projects, interfaces may offer better incremental compilation performance. When modifying type definitions, TypeScript only needs to recheck affected interfaces, whereas type aliases may trigger more extensive rechecks.

Type Compatibility

In the type system, interfaces and type aliases follow essentially the same compatibility rules:

interface IUser {
  name: string;
}
type TUser = {
  name: string;
};

let iUser: IUser = { name: 'Alice' };
let tUser: TUser = { name: 'Bob' };

iUser = tUser; // Compatible
tUser = iUser; // Compatible

Type Extension Patterns

This pattern is common in open-source library type definitions:

// Core library type
interface Request {
  url: string;
}

// User extension
interface Request {
  method?: string;
}

Type aliases cannot achieve this extension pattern but can simulate it via intersection types:

type Request = {
  url: string;
} & {
  method?: string;
};

Type Operators

Type aliases can work with more type operators:

type Nullable<T> = T | null;
type NonNullable<T> = T extends null | undefined ? never : T;

Practical Examples of Type Inference

In React components, type aliases can simplify props type extraction:

type PropsOf<T> = T extends React.ComponentType<infer P> ? P : never;
type MyComponentProps = PropsOf<typeof MyComponent>;

Type and Value Namespaces

In TypeScript, types and values exist in different namespaces. Interface-created type names exist in both the type and value spaces, while type alias-created names exist only in the type space.

interface I {
  x: number;
}
class C implements I {
  x = 0;
}

type T = {
  x: number;
};
class D implements T {
  x = 0;
}

Type Parameter Defaults

Type aliases support more flexible type parameter defaults:

type Container<T = string> = { value: T };

Interfaces also support type parameter defaults:

interface Container<T = string> {
  value: T;
}

Type Predicates

Both behave the same in custom type guards:

interface Cat {
  meow(): void;
}
type Dog = {
  bark(): void;
};

function isCat(pet: Cat | Dog): pet is Cat {
  return (pet as Cat).meow !== undefined;
}

Flexibility in Type Extension

Type aliases can implement more complex extension logic via conditional types:

type Exclude<T, U> = T extends U ? never : T;
type Extract<T, U> = T extends U ? T : never;

Mutual References Between Types and Interfaces

Type aliases and interfaces can reference each other:

interface Shape {
  type: 'circle' | 'square';
  radius?: number;
  sideLength?: number;
}

type Circle = Shape & {
  type: 'circle';
  radius: number;
};

Depth in Type Queries

Type aliases handle deep type queries better:

type User = {
  name: string;
  address: {
    street: string;
    geo: [number, number];
  };
};
type Geo = User['address']['geo']; // [number, number]

Type Operations

Type aliases support richer type operations:

type A = { a: number };
type B = { b: string };
type C = A & B; // { a: number; b: string }
type D = keyof C; // 'a' | 'b'

Selection Strategy for Types and Interfaces

  1. Prefer interfaces for public API definitions to facilitate user extension.
  2. Use type aliases for component Props and State, especially when union types are needed.
  3. Type aliases' intersection types are more intuitive when combining multiple types.
  4. Interfaces are mandatory when declaration merging is required.
  5. Complex type operations must use type aliases.

本站部分内容来自互联网,一切版权均归源网站或源作者所有。

如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn

Front End Chuan

Front End Chuan, Chen Chuan's Code Teahouse 🍵, specializing in exorcising all kinds of stubborn bugs 💻. Daily serving baldness-warning-level development insights 🛠️, with a bonus of one-liners that'll make you laugh for ten years 🐟. Occasionally drops pixel-perfect romance brewed in a coffee cup ☕.