VDone Demo VDone Demo
Home
  • Articles

    • JavaScript
  • Study Notes

    • JavaScript Tutorial
    • Professional JavaScript
    • ES6 Tutorial
    • Vue
    • React
    • TypeScript: Build Axios from Scratch
    • Git
    • TypeScript
    • JS Design Patterns
  • HTML
  • CSS
  • Technical Docs
  • GitHub Tips
  • Node.js
  • Blog Setup
  • Learning
  • Interviews
  • Miscellaneous
  • Practical Tips
  • Friends
About
Bookmarks
  • Categories
  • Tags
  • Archives
GitHub (opens new window)

Nikolay Tuzov

Backend Developer
Home
  • Articles

    • JavaScript
  • Study Notes

    • JavaScript Tutorial
    • Professional JavaScript
    • ES6 Tutorial
    • Vue
    • React
    • TypeScript: Build Axios from Scratch
    • Git
    • TypeScript
    • JS Design Patterns
  • HTML
  • CSS
  • Technical Docs
  • GitHub Tips
  • Node.js
  • Blog Setup
  • Learning
  • Interviews
  • Miscellaneous
  • Practical Tips
  • Friends
About
Bookmarks
  • Categories
  • Tags
  • Archives
GitHub (opens new window)
  • 初识 TypeScript

  • TypeScript 常用语法

    • Basic Types
    • Variable Declarations
    • Interfaces
      • A First Look at Interfaces
      • Optional Properties
      • Readonly Properties
        • readonly vs const
      • Excess Property Checks
      • Function Types
      • Indexable Types
      • Class Types
        • Implementing an Interface
        • Difference Between Static and Instance Sides of Classes
      • Extending Interfaces
      • Hybrid Types
      • Interfaces Extending Classes
    • Classes
    • Functions
    • Generics
    • Type Inference
    • Advanced Types
  • ts-axios 项目初始化

  • ts-axios 基础功能实现

  • ts-axios 异常情况处理

  • ts-axios 接口扩展

  • ts-axios 拦截器实现

  • ts-axios 配置化实现

  • ts-axios 取消功能实现

  • ts-axios 更多功能实现

  • ts-axios 单元测试

  • ts-axios 部署与发布

  • 《TypeScript 从零实现 axios》
  • TypeScript 常用语法
HuangYi
2020-01-05
Contents

Interfaces

# Interfaces

One of TypeScript's core principles is that type checking focuses on the shape that values have. This is sometimes called "duck typing" or "structural subtyping." In TypeScript, interfaces serve the role of naming these types and defining contracts within your code or with third-party code.

# A First Look at Interfaces

Let's observe how interfaces work with a simple example:

function printLabel(labelledObj: { label: string }) {
  console.log(labelledObj.label)
}

let myObj = { size: 10, label: 'Size 10 Object' }
printLabel(myObj)
1
2
3
4
5
6

The type checker examines the call to printLabel. printLabel has a single parameter that requires the passed object to have a property called label of type string. Note that the object we pass in may actually have more properties, but the compiler only checks that at least the required ones are present and their types match. However, there are cases where TypeScript is not so lenient, which we'll briefly discuss.

Let's rewrite the above example, this time using an interface to describe the requirement of having a label property of type string:

interface LabelledValue {
  label: string
}

function printLabel(labelledObj: LabelledValue) {
  console.log(labelledObj.label)
}

let myObj = {size: 10, label: 'Size 10 Object'}
printLabel(myObj)
1
2
3
4
5
6
7
8
9
10

The LabelledValue interface serves as a name describing the structure in the example above. It represents an object that has a label property of type string. Note that unlike in some other languages, we can't say that the object passed to printLabel implements this interface. We only care about the shape of the value. As long as the passed object meets the requirements mentioned above, it is allowed.

One more thing worth mentioning is that the type checker doesn't check property order -- only that the corresponding properties exist and their types are correct.

# Optional Properties

Not all properties of an interface are required. Some exist only under certain conditions, or may not exist at all. For example, when passing an object to a function where only some properties are assigned:

interface Square {
  color: string,
  area: number
}

interface SquareConfig {
  color?: string
  width?: number
}

function createSquare (config: SquareConfig): Square {
  let newSquare = {color: 'white', area: 100}
  if (config.color) {
    newSquare.color = config.color
  }
  if (config.width) {
    newSquare.area = config.width * config.width
  }
  return newSquare
}

let mySquare = createSquare({color: 'black'})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

Interfaces with optional properties are similar to regular interface definitions, except a ? is appended after the property name.

One advantage of optional properties is that you can pre-define properties that might exist, and another is that you can catch errors when referencing properties that don't exist. For example, if we intentionally misspell the color property inside createSquare, we'll get an error:

interface Square {
  color: string,
  area: number
}

interface SquareConfig {
   color?: string;
   width?: number;
}

function createSquare(config: SquareConfig): Square {
   let newSquare = {color: 'white', area: 100}
   if (config.clor) {
     // Error: Property 'clor' does not exist on type 'SquareConfig'
     newSquare.color = config.clor
   }
   if (config.width) {
     newSquare.area = config.width * config.width
   }
   return newSquare
 }

 let mySquare = createSquare({color: 'black'})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# Readonly Properties

Some object properties can only be modified when the object is first created. You can specify read-only properties by putting readonly before the property name:

interface Point {
  readonly x: number
  readonly y: number
}
1
2
3
4

You can construct a Point by assigning an object literal. After assignment, x and y can never be changed.

let p1: Point = { x: 10, y: 20 }
p1.x = 5 // error!
1
2

TypeScript provides ReadonlyArray<T>, which is similar to Array<T> but with all mutating methods removed, ensuring arrays can never be modified after creation:

let a: number[] = [1, 2, 3, 4]
let ro: ReadonlyArray<number> = a
ro[0] = 12 // error!
ro.push(5) // error!
ro.length = 100 // error!
a = ro // error!
1
2
3
4
5
6

On the last line above, you can see that even assigning the entire ReadonlyArray back to a regular array is not allowed. But you can override it with a type assertion:

a = ro as number[]
1

# readonly vs const

The simplest way to remember whether to use readonly or const is to ask whether you're using it as a variable or a property. Use const for variables and readonly for properties.

# Excess Property Checks

In our first example, we used interfaces, and TypeScript let us pass { size: number; label: string; } to a function expecting only { label: string; }, and we already learned about optional properties.

However, naively combining the two can trip you up, just as it would in JavaScript. For example, using the createSquare example:

interface SquareConfig {
    color?: string;
    width?: number;
}

function createSquare (config: SquareConfig): { color: string; area: number } {
  let newSquare = {color: 'white', area: 100}
  if (config.color) {
    newSquare.color = config.color
  }
  if (config.width) {
    newSquare.area = config.width * config.width
  }
  return newSquare
}


let mySquare = createSquare({ colour: 'red', width: 100 })
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

Notice that the argument to createSquare is spelled colour instead of color. In JavaScript, this would silently fail.

You might argue that this program is correctly typed because the width property is compatible, there's no color property, and the extra colour property is insignificant.

However, TypeScript considers this code potentially buggy. Object literals get special treatment and undergo excess property checking when assigning them to variables or passing them as arguments. If an object literal has any properties that the "target type" doesn't have, you'll get an error.

// error: 'colour' does not exist on type 'SquareConfig'
let mySquare = createSquare({ colour: 'red', width: 100 })
1
2

Getting around these checks is straightforward. The simplest method is to use a type assertion:

let mySquare = createSquare({ width: 100, opacity: 0.5 } as SquareConfig)
1

However, the best approach is to add a string index signature, provided you're sure the object may have extra properties used for special purposes. If SquareConfig can have color and width properties with the types defined above, but can also have any number of other properties, we can define it as:

interface SquareConfig {
  color?: string
  width?: number
  [propName: string]: any
}
1
2
3
4
5

We'll discuss index signatures later, but here we're saying that SquareConfig can have any number of properties, and as long as they aren't color or width, their types don't matter.

There's one last way to bypass these checks, which may surprise you -- assign the object to another variable: since squareOptions won't undergo excess property checking, the compiler won't report an error.

let squareOptions = { colour: 'red', width: 100 }
let mySquare = createSquare(squareOptions)
1
2

Keep in mind that in simple code like the above, you probably shouldn't try to bypass these checks. For complex object literals with methods and internal state, you might need these techniques, but most excess property check errors are real bugs. That said, if you encounter an excess property check error, you should review your type declarations. Here, if passing either color or colour to createSquare is supported, you should update the SquareConfig definition to reflect that.

# Function Types

Interfaces can describe the wide variety of shapes that JavaScript objects can take. In addition to describing objects with properties, interfaces can also describe function types.

To describe a function type with an interface, we define a call signature. It's like a function definition with only the parameter list and return type. Each parameter in the parameter list requires both a name and a type.

interface SearchFunc {
  (source: string, subString: string): boolean
}
1
2
3

Once defined, we can use this function type interface like other interfaces. The example below shows how to create a function type variable and assign a function of the same type to it.

let mySearch: SearchFunc
mySearch = function(source: string, subString: string): boolean {
  let result = source.search(subString);
  return result > -1
}
1
2
3
4
5

For function type checking, the parameter names don't need to match those defined in the interface. For example, we could rewrite the above example as:

let mySearch: SearchFunc
mySearch = function(src: string, sub: string): boolean {
  let result = src.search(sub);
  return result > -1
}
1
2
3
4
5

Function parameters are checked one at a time, with each parameter at a corresponding position requiring compatible types. If you don't want to specify types, TypeScript's type system will infer the parameter types because the function is directly assigned to a SearchFunc type variable. The function's return type is inferred from its return value (in this case, false and true). If the function returned a number or string, the type checker would warn that the return type doesn't match the definition in the SearchFunc interface.

let mySearch: SearchFunc
mySearch = function(src, sub) {
  let result = src.search(sub)
  return result > -1
}
1
2
3
4
5

# Indexable Types

Similar to using interfaces to describe function types, we can also describe types that can be "indexed into" like a[10] or ageMap['daniel']. Indexable types have an index signature that describes the types used to index into the object, along with the corresponding return types. Let's look at an example:

interface StringArray {
  [index: number]: string
}

let myArray: StringArray
myArray = ['Bob', 'Fred']

let myStr: string = myArray[0]
1
2
3
4
5
6
7
8

Above, we defined a StringArray interface with an index signature. This index signature states that when StringArray is indexed with a number, it returns a string.

TypeScript supports two types of index signatures: string and number. You can use both types of indexers simultaneously, but the return type of the numeric indexer must be a subtype of the string indexer's return type. This is because when indexing with a number, JavaScript converts it to a string before indexing into the object. That means indexing with 100 (a number) is the same as indexing with '100' (a string), so the two need to be consistent.

class Animal {
  name: string
}
class Dog extends Animal {
  breed: string
}

// Error: indexing with a numeric string index might get a completely different Animal!
interface NotOkay {
  [x: number]: Animal
  [x: string]: Dog
}
1
2
3
4
5
6
7
8
9
10
11
12

String index signatures effectively describe the dictionary pattern, and they also enforce that all properties match their return type. Because string indexing declares that both obj.property and obj['property'] are valid. In the following example, name's type doesn't match the string index type, so the type checker gives an error:

interface NumberDictionary {
  [index: string]: number;
  length: number;    // ok, length is a number
  name: string       // error, name's type doesn't match the index return type
}
1
2
3
4
5

Finally, you can make index signatures readonly to prevent assignment to indices:

interface ReadonlyStringArray {
  readonly [index: number]: string;
}
let myArray: ReadonlyStringArray = ['Alice', 'Bob'];
myArray[2] = 'Mallory'; // error!
1
2
3
4
5

# Class Types

# Implementing an Interface

As in C# or Java, TypeScript can also be used to explicitly force a class to conform to a particular contract.

interface ClockInterface {
  currentTime: Date
}

class Clock implements ClockInterface {
  currentTime: Date
  constructor(h: number, m: number) { }
}
1
2
3
4
5
6
7
8

You can also describe a method in the interface and implement it in the class, as with the setTime method below:

interface ClockInterface {
  currentTime: Date
  setTime(d: Date)
}

class Clock implements ClockInterface {
  currentTime: Date
  setTime(d: Date) {
    this.currentTime = d
  }
  constructor(h: number, m: number) { }
}
1
2
3
4
5
6
7
8
9
10
11
12

Interfaces describe the public side of the class, rather than both the public and private sides. They won't help you check whether the class also has particular private members.

# Difference Between Static and Instance Sides of Classes

When working with classes and interfaces, keep in mind that a class has two types: the static side type and the instance type. You'll notice that when you define an interface with a constructor signature and try to define a class that implements it, you'll get an error:

interface ClockConstructor {
  new (hour: number, minute: number)
}

// error
class Clock implements ClockConstructor {
  currentTime: Date
  constructor(h: number, m: number) { }
}
1
2
3
4
5
6
7
8
9

This is because when a class implements an interface, only the instance side is checked. The constructor exists on the static side, so it's not included in the check.

Instead, look at the following example. We define two interfaces: ClockConstructor for the constructor and ClockInterface for the instance methods. For convenience, we define a factory function createClock that creates instances with the passed-in type.

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

function createClock(ctor: ClockConstructor, hour: number, minute: number): ClockInterface {
  return new ctor(hour, minute)
}

class DigitalClock implements ClockInterface {
  constructor(h: number, m: number) { }
  tick() {
    console.log('beep beep')
  }
}
class AnalogClock implements ClockInterface {
  constructor(h: number, m: number) { }
  tick() {
    console.log('tick tock')
  }
}

let digital = createClock(DigitalClock, 12, 17)
let analog = createClock(AnalogClock, 7, 32)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

Because createClock's first parameter is of type ClockConstructor, in createClock(AnalogClock, 7, 32), it checks that AnalogClock conforms to the constructor signature.

# Extending Interfaces

Like classes, interfaces can extend each other. This allows us to copy members from one interface to another, giving us more flexibility in splitting interfaces into reusable modules.

interface Shape {
  color: string
}

interface Square extends Shape {
  sideLength: number
}

let square = {} as Square
square.color = 'blue'
square.sideLength = 10
1
2
3
4
5
6
7
8
9
10
11

An interface can extend multiple interfaces, creating a combination of all the interfaces.

interface Shape {
  color: string
}

interface PenStroke {
  penWidth: number
}

interface Square extends Shape, PenStroke {
  sideLength: number
}

let square = {} as Square
square.color = 'blue'
square.sideLength = 10
square.penWidth = 5.0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# Hybrid Types

As we mentioned earlier, interfaces can describe the rich variety of types found in JavaScript. Because of JavaScript's dynamic and flexible nature, you may occasionally want an object to work as a combination of the types described above.

One example is an object that acts as both a function and an object, with additional properties.

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

function getCounter(): Counter {
  let counter = (function (start: number) { }) as Counter
  counter.interval = 123
  counter.reset = function () { }
  return counter
}

let c = getCounter()
c(10)
c.reset()
c.interval = 5.0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

When working with third-party JavaScript libraries, you may need to fully define types like this. The axios library we'll be refactoring in this course is a great example.

# Interfaces Extending Classes

When an interface extends a class type, it inherits the members of the class but not their implementations. It's as if the interface declared all of the members of the class without providing an implementation. Interfaces also inherit the private and protected members of a class. This means that when you create an interface that extends a class with private or protected members, that interface type can only be implemented by that class or a subclass of it.

This is useful when you have a large inheritance hierarchy, but the point is that your code only works on subclasses with certain properties. These subclasses have no relation to the base class except inheriting from it. Example:

class Control {
  private state: any
}

interface SelectableControl extends Control {
  select(): void
}

class Button extends Control implements SelectableControl {
  select() { }
}

class TextBox extends Control {
  select() { }
}

// Error: Type "ImageC" is missing property "state".
class ImageC implements SelectableControl {
  select() { }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

In the above example, SelectableControl contains all members of Control, including the private member state. Since state is a private member, only subclasses of Control can implement SelectableControl. This is because only subclasses of Control will have a private member state declared in Control, which is required for private member compatibility.

Within the Control class, it's possible to access the private member state through an instance of SelectableControl. Effectively, SelectableControl is the same as a Control class with a select method. The Button and TextBox classes are subtypes of SelectableControl (because they both inherit from Control and have a select method), but the ImageC class is not.

Edit (opens new window)
#TypeScript
Last Updated: 2026/03/21, 12:14:36
Variable Declarations
Classes

← Variable Declarations Classes→

Recent Updates
01
How I Discovered Disposable Email — A True Story
06-12
02
Animations in Grid Layout
09-15
03
Renaming a Git Branch
08-11
More Articles >
Theme by VDone | Copyright © 2026-2026 Nikolay Tuzov | MIT License | Telegram
  • Auto
  • Light Mode
  • Dark Mode
  • Reading Mode