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
    • Classes
    • Functions
    • Generics
    • Type Inference
    • Advanced Types
      • Intersection Types
      • Union Types
      • Type Guards
        • User-Defined Type Guards
        • typeof Type Guards
        • instanceof Type Guards
      • Nullable Types
        • Optional Parameters and Optional Properties
        • Type Guards and Type Assertions
      • String Literal Types
      • Summary
  • 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

Advanced Types

# Advanced Types

# Intersection Types

An intersection type combines multiple types into one. This lets us merge existing types together into a single type that has all the features we need. For example, Person & Loggable is both a Person and Loggable. That means an object of this type has all the members of both types.

We mostly see intersection types used in mixins or other patterns that don't fit the typical object-oriented model. (This happens a lot in JavaScript!) Here's a simple example of how to create a mixin:

function extend<T, U> (first: T, second: U): T & U {
  let result = {} as T & U
  for (let id in first) {
    result[id] = first[id] as any
  }
  for (let id in second) {
    if (!result.hasOwnProperty(id)) {
      result[id] = second[id] as any
    }
  }
  return result
}

class Person {
  constructor (public name: string) {
  }
}

interface Loggable {
  log (): void
}

class ConsoleLogger implements Loggable {
  log () {
    // ...
  }
}

var jim = extend(new Person('Jim'), new ConsoleLogger())
var n = jim.name
jim.log()
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
27
28
29
30
31

# Union Types

Union types are closely related to intersection types but are used very differently. Occasionally you'll encounter a situation where a library expects a number or string parameter. For example, the following function:

function padLeft(value: string, padding: any) {
  if (typeof padding === 'number') {
    return Array(padding + 1).join(' ') + value
  }
  if (typeof padding === 'string') {
    return padding + value
  }
  throw new Error(`Expected string or number, got '${padding}'.`)
}

padLeft('Hello world', 4) // returns "    Hello world"

1
2
3
4
5
6
7
8
9
10
11
12

The problem with padLeft is that the padding parameter is typed as any. This means we can pass in a value that is neither number nor string, and TypeScript won't complain.

let indentedString = padLeft('Hello world', true) // compiles but throws at runtime
1

To solve this, we can use a union type for the padding parameter:

function padLeft(value: string, padding: string | number) {
  // ...
}

let indentedString = padLeft('Hello world', true) // error at compile time
1
2
3
4
5

A union type describes a value that can be one of several types. We use the vertical bar (|) to separate each type, so number | string means a value can be either a number or a string.

If a value is a union type, we can only access members that are common to all types in the union.

interface Bird {
  fly()
  layEggs()
}

interface Fish {
  swim()
  layEggs()
}

function getSmallPet(): Fish | Bird {
  // ...
}

let pet = getSmallPet()
pet.layEggs() // okay
pet.swim()    // error
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

Union types can be a bit complex: if a value has type A | B, we can only be sure it has members common to both A and B. In this example, Fish has a swim method, but we can't be sure a variable of type Bird | Fish has swim. If the variable is Bird at runtime, calling pet.swim() would be an error.

# Type Guards

Union types are useful for cases where a value can be different types. But what do we do when we want to know specifically whether it's a Fish or a Bird? A common JavaScript idiom is to check for the existence of a member. As mentioned, we can only access members that are common to all types in a union.

let pet = getSmallPet()

// Each member access will cause an error
if (pet.swim) {
  pet.swim()
} else if (pet.fly) {
  pet.fly()
}
1
2
3
4
5
6
7
8

To make this code work, we need to use type assertions:

let pet = getSmallPet()

if ((pet as Fish).swim) {
  (pet as Fish).swim()
} else {
  (pet as Bird).fly()
}
1
2
3
4
5
6
7

# User-Defined Type Guards

Notice that we had to use type assertions multiple times. It would be much better if once we checked the type, we could know the type of pet within each branch.

TypeScript's type guard mechanism makes this possible. A type guard is an expression that performs a runtime check to guarantee the type in some scope. To define a type guard, we simply define a function whose return value is a type predicate:

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

In this example, pet is Fish is the type predicate. A predicate takes the form parameterName is Type, where parameterName must be a parameter name from the current function signature.

Whenever isFish is called with some variable, TypeScript narrows that variable to the specific type.

if (isFish(pet)) {
  pet.swim()
}
else {
  pet.fly()
}
1
2
3
4
5
6

Note that TypeScript not only knows that pet is Fish in the if branch; it also knows that in the else branch, it is definitely not Fish but rather Bird.

# typeof Type Guards

Now let's go back to see how to write padLeft code using union types. We could use type assertions like this:

function isNumber (x: any):x is string {
  return typeof x === 'number'
}

function isString (x: any): x is string {
  return typeof x === 'string'
}

function padLeft (value: string, padding: string | number) {
  if (isNumber(padding)) {
    return Array(padding + 1).join(' ') + value
  }
  if (isString(padding)) {
    return padding + value
  }
  throw new Error(`Expected string or number, got '${padding}'.`)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

However, having to define a function to determine whether a type is a primitive type isn't necessary. We don't actually need to abstract typeof x === 'number' into a function because TypeScript recognizes it as a type guard. This means we can check types directly in code.

function padLeft (value: string, padding: string | number) {
  if (typeof padding === 'number') {
    return Array(padding + 1).join(' ') + value
  }
  if (typeof padding === 'string') {
    return padding + value
  }
  throw new Error(`Expected string or number, got '${padding}'.`)
}
1
2
3
4
5
6
7
8
9

These typeof type guards are recognized in two forms: typeof v === "typename" and typeof v !== "typename", where "typename" must be "number", "string", "boolean", or "symbol". TypeScript won't stop you from comparing against other strings, but it won't recognize those expressions as type guards.

# instanceof Type Guards

If you've read about typeof type guards and are familiar with JavaScript's instanceof operator, you can probably already guess what this section covers.

instanceof type guards are a way to narrow types using constructor functions. Let's modify the earlier example slightly:

class Bird {
  fly () {
    console.log('bird fly')
  }

  layEggs () {
    console.log('bird lay eggs')
  }
}

class Fish {
  swim () {
    console.log('fish swim')
  }

  layEggs () {
    console.log('fish lay eggs')
  }
}

function getRandomPet () {
  return Math.random() > 0.5 ? new Bird() : new Fish()
}

let pet = getRandomPet()

if (pet instanceof Bird) {
  pet.fly()
}
if (pet instanceof Fish) {
  pet.swim()
}
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
27
28
29
30
31
32

# Nullable Types

TypeScript has two special types, null and undefined, which have the values null and undefined respectively. We briefly mentioned them in the Basic Types section. By default, the type checker considers null and undefined assignable to anything. null and undefined are valid values of all other types. This also means you can't prevent them from being assigned to any type, even if you'd like to. The inventor of null, Tony Hoare, called it the billion-dollar mistake (opens new window).

The --strictNullChecks flag can fix this: when you declare a variable, it won't automatically include null or undefined. You can explicitly include them using union types:

let s = 'foo'
s = null // error, 'null' is not assignable to 'string'
let sn: string | null = 'bar'
sn = null // ok

sn = undefined // error, 'undefined' is not assignable to 'string | null'
1
2
3
4
5
6

Note that TypeScript treats null and undefined differently per JavaScript semantics. string | null, string | undefined, and string | undefined | null are different types.

# Optional Parameters and Optional Properties

With --strictNullChecks, optional parameters automatically get | undefined added:

function f(x: number, y?: number) {
  return x + (y || 0)
}
f(1, 2)
f(1)
f(1, undefined)
f(1, null) // error, 'null' is not assignable to 'number | undefined'
1
2
3
4
5
6
7

Optional properties are handled the same way:

class C {
  a: number
  b?: number
}
let c = new C()
c.a = 12
c.a = undefined // error, 'undefined' is not assignable to 'number'
c.b = 13
c.b = undefined // ok
c.b = null // error, 'null' is not assignable to 'number | undefined'
1
2
3
4
5
6
7
8
9
10

# Type Guards and Type Assertions

Since nullable types can be defined as union types with other types, you need to use type guards to remove null. Fortunately, this is the same as what you'd write in JavaScript:

function f(sn: string | null): string {
  if (sn === null) {
    return 'default'
  } else {
    return sn
  }
}
1
2
3
4
5
6
7

This clearly eliminates null. You can also use the short-circuit operator:

function f(sn: string | null): string {
  return sn || 'default'
}
1
2
3

If the compiler can't eliminate null or undefined, you can use a type assertion to manually remove them. The syntax adds a ! postfix: identifier! removes null and undefined from the type of identifier:

function broken(name: string | null): string {
  function postfix(epithet: string) {
    return name.charAt(0) + '.  the ' + epithet // error, 'name' is possibly null
  }
  name = name || 'Bob'
  return postfix('great')
}

function fixed(name: string | null): string {
  function postfix(epithet: string) {
    return name!.charAt(0) + '.  the ' + epithet // ok
  }
  name = name || 'Bob'
  return postfix('great')
}

broken(null)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

This example uses a nested function because the compiler can't eliminate null inside nested functions (unless it's an immediately-invoked function expression). This is because it can't track all calls to the nested function, especially if the inner function is returned from the outer function. Without knowing where the function is called, it can't know the type of name at call time.

# String Literal Types

String literal types allow you to specify the exact value a string must have. In practice, string literal types combine well with union types and type guards. By combining these features, you can achieve enum-like behavior with strings.

type Easing = 'ease-in' | 'ease-out' | 'ease-in-out'

class UIElement {
  animate (dx: number, dy: number, easing: Easing) {
    if (easing === 'ease-in') {
      // ...
    } else if (easing === 'ease-out') {
    } else if (easing === 'ease-in-out') {
    } else {
      // error! should not pass null or undefined.
    }
  }
}

let button = new UIElement()
button.animate(0, 0, 'ease-in')
button.animate(0, 0, 'uneasy') // error

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

You can only choose one of three allowed strings as the argument. Passing any other value produces an error.

Argument of type '"uneasy"' is not assignable to parameter of type '"ease-in" | "ease-out" | "ease-in-out"'
1

# Summary

That concludes our study of commonly used TypeScript syntax. Of course, TypeScript has other syntax we didn't cover -- we only discussed the most commonly used parts, which are already sufficient for developing typical applications. If you encounter other TypeScript syntax when developing projects, you can learn about it through the TypeScript official documentation (opens new window). The best way to learn the basics is to read the official docs and try the examples. In fact, the structure of our course's foundational material largely follows the official documentation. Remember that when learning any technology, the official documentation is always the best primary resource.

But learning TypeScript can't rely solely on reading official docs -- you also need hands-on practice. Only through practice can you truly master TypeScript. I'm sure many of you are eager to put your knowledge to use, so let's start turning theory into practice and use TypeScript to rebuild axios together!

Edit (opens new window)
#TypeScript
Last Updated: 2026/03/21, 12:14:36
Type Inference
Requirements Analysis

← Type Inference Requirements Analysis→

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