返回文章列表

TypeScript 学习之旅:我的学习笔记

一份完整的 TypeScript 学习笔记合集,从基础概念到高级特性

发布于 2023年5月12日·12 分钟

简介

TypeScript 是静态类型语言,可以在编译期就发现类型错误。它专为客户端设计,而 JavaScript 在服务端和客户端都能运行。

  • JavaScript 的替代方案(超集)
  • 允许我们使用严格的类型
  • 支持现代语法特性(箭头函数、let、const)
  • 提供额外特性(泛型、接口、元组等)

安装

npm install -g typescript

编译

TypeScript 文件和 JavaScript 文件使用不同的文件名编译:

tsc filename.ts filename2.js

TypeScript 文件和 JavaScript 文件使用相同的文件名编译:

tsc filename.ts

监听文件变化并自动重新编译:

tsc filename.ts -w

类型基础

在 TypeScript 中,变量的类型一旦确定,就不能再更改。

// No need to specify types; TypeScript infers the type based on the assigned value.
let name = 'yooumuu'; // String
let age = 30; // Number
let isCool = false; // Boolean

// Strings can use double or single quotes, just like in JavaScript.
let character = 'mario';
// character = 30; // Error, strings can only be reassigned to strings, the same applies to other data types.
character = 'luigi'; // ✅

TypeScript 在编译阶段进行类型检查,能在代码运行之前就拦截类型错误。

const circ = (diameter: number) => {
  return diameter * Math.PI;
};
// console.log(circ("hello")); // Error, passing a non-number argument is detected at compile time
console.log(circ(7.5)); // ✅

对象与数组

数组

let names = ['luigi', 'mario', 'yoshi'];

// names = "hello"; // Error, variable type cannot change

// Once a type is assigned to an array, it cannot be changed.
// If the initial array contains strings, only strings can be added later. The same rule applies to numbers and booleans.
names.push('toad');
// name.push(3); // Error
// name[0] = 3; // Error
// names = [0, 1] // Error

// Mixed arrays can include types already present in the array.
let mixed = ['ken', 4, 'chun-li', 8, 9];
mixed.push('ryu');
mixed.push(10);
mixed[0] = 3; // Changing a string to a number is allowed
// mixed.push(false); // Error

对象

let character = {
  name: 'mario',
  color: 'red',
  age: 30,
};

// Similarly, object property types cannot be changed once assigned.
character.age = 40;
character.name = 'ryu';
// character.age = "30"; // Error

// You cannot add properties that were not initially defined.
// character.skills = ["fighting", "sneaking"]; // Error

// When reassigning an object, it must have the same structure, property names, and the same number of properties.
character = {
  name: 'yoshi',
  color: 'green',
  age: 34,
};

显式类型与联合类型

显式类型

let character: string;
let age: number;
let isLoggedIn: boolean;

// age = "luigi"; // Error
isLoggedIn = false;

// Arrays
let characters: string[] = []; // Specify an array containing strings and initialize it as an empty array.
characters.push('shaun');
// characters = [0, 1]; // Error
characters = ['mario', 'yoshi'];

联合类型

let uid: string | number;
uid = '123';
uid = 123;
// uid = false; // Error

// Arrays
let mixed: (string | number)[] = [];
mixed.push('hello');
mixed.push(12);

// Objects
let characterOne: object;
characterOne = { name: 'yoshi', age: 30 };
characterOne = []; // Arrays are a special kind of object
// characterOne = " "; // Error

// Explicitly specify that a variable is an object and specify the types of its properties:
// let characterTwo: {};
let characterTwo: {
  name: string;
  age: number;
  color: string;
};

characterTwo = {
  name: 'mario',
  age: 30,
  color: 'red',
};

动态类型(any)

注意:使用 any 类型时要格外谨慎。

let age: any = 25;
any = true;
any = 'hello';
any = { name: 'luigi' };

let mixed: any[] = [];
mixed.push(5);
mixed.push('mario');
mixed.push(false);

let character: {
  name: any;
  age: any;
};
character = { name: 'yoshi', age: 25 };
character = { name: 25, age: 'yoshi' };

函数基础

// Automatically inferred as a function type.
let greet = () => {
  console.log('hello, world');
};
// greet = "hello"; // Error

let greet2: Function; // Specify the variable type as a function with a capital 'F'.
greet2 = () => {
  console.log('hello, again');
};

// Define optional parameters
const add = (a: number, b: number, c?: number | string) => {
  console.log(a + b);
  console.log(c); // undefined
};

// Define default parameters
const minus = (a: number, b: number, c: number | string = 10) => {
  console.log(a - b);
  console.log(c); // 10
};

// Return type is automatically inferred; if no return, it's inferred as 'void.'
const minus2 = (a: number, b: number, c: number | string = 10): number => {
  return a - b;
};
let result = minus2(10, 7); // TypeScript infers 'result' as a number
// result = 'something else'; // Error

类型别名

// Before:
const logDetails = (uid: string | number, item: string) => {
  console.log(`${item} has a uid of ${uid}`);
};

const greet = (user: { name: string; uid: string | number }) => {
  console.log(`${user.name} says hello`);
};

// After:
type StringOrNum = string | number;
type objWithName = { name: string; uid: StringOrNum };

const logDetails = (uid: StringOrNum, item: string) => {
  console.log(`${item} has a uid of ${uid}`);
};

const greet = (user: objWithName) => {
  console.log(`${user.name} says hello`);
};

函数签名

// Example 1
let greet: (a: string, b: string) => void; // 'greet' is a function that takes two string arguments and returns 'void.'

greet = (name: string, greeting: string) => {
  console.log(`${name} says ${greeting}`);
};

// Example 2
let calc: (a: number, b: number, c: string) => number; // 'calc' is a function that takes two number arguments and a string argument and returns a number.

calc = (numOne: number, numTwo: number, action: string) => {
  if (action === 'add') {
    return numOne + numTwo;
  } else {
    // Since the return type is number, there must be an 'else' statement.
    return numOne - numTwo;
  }
};

// Example 3
let logDetails: (obj: { name: string; age: number }) => void; // 'logDetails' is a function that takes an object with 'name' and 'age' properties and returns 'void.'
// Combined with type aliases
type person = { name: string; age: number };
logDetails = (character: person) => {
  console.log(`${character.name} is ${character.age} years old`);
};

DOM 交互与类型断言

const anchor = document.querySelector('a');
console.log(anchor); // <a href="https://www.google.com">Google</a>
// console.log(anchor.href); // Error, TypeScript doesn't know the type of this element, so you can't directly use the 'href' property.

// Use type casting to inform TypeScript about the element's type.
// 1. Use an if/else statement.
if (anchor) {
  console.log(anchor.href);
}

// 2. Use '!' after variable assignment to assert that it is not null.
const anchor2 = document.querySelector('a')!;
console.log(anchor2.href); // ✅
// TypeScript recognizes this variable as an HTMLAnchorElement, allowing auto-completion and type-specific methods/properties.
// const form = document.querySelector('form')!;
const form = document.querySelector('.new-item-form') as HTMLFormElement;
// When using class, id, tag name selectors, TypeScript automatically infers them as HTMLElements, so you need to manually specify the type using 'as HTMLFormElement'. You don't need to add '!' because TypeScript knows the variable is not null.
console.log(form.children); // HTMLCollection [input#type, input#tofrom, input#details, button, button]
// Inputs
const type = document.querySelector('#type') as HTMLSelectElement;
const tofrom = document.querySelector('#tofrom') as HTMLInputElement;
const details = document.querySelector('#details') as HTMLInputElement;
const amount = document.querySelector('#amount') as HTMLInputElement;

form.addEventListener('submit', (e: Event) => {
  e.preventDefault();
  console.log(type.value, tofrom.value, details.value, amount.valueAsNumber);
});
// Use 'valueAsNumber' to directly retrieve numeric input values instead of strings.

类

class Invoice {
  // Define properties of this class
  client: string;
  details: string;
  amount: number;

  // Initialize properties in the constructor
  constructor(c: string, d: string, a: number) {
    this.client = c;
    this.details = d;
    this.amount = a;
  }

  // Define methods of this class
  format() {
    return `${this.client} owes $${this.amount} for ${this.details}`;
  }
}

// Instantiate objects of this class
const invOne = new Invoice('mario', 'work on the mario website', 250);
const invTwo = new Invoice('luigi', 'work on the luigi website', 300);

let invoices: Invoice[] = []; // Specify that this array contains Invoice objects

// Default class property access is 'public'; you can access and modify it outside the class.
invOne.client = 'yoshi';
invTwo.amount = 400;

Public、Private 与 Readonly

默认情况下,类的属性都是 public 的,可以在类的外部访问和修改。可以通过 private、public 和 readonly 来改变这一行为。

class Invoice {
  // Define properties of this class
  readonly client: string; // Readonly property, cannot be modified
  private details: string; // Private property, can only be accessed within the class
  public amount: number; // Public property, can be accessed and modified outside the class

  // Initialize properties in the constructor
  constructor(c: string, d: string, a: number) {
    this.client = c;
    this.details = d;
    this.amount = a;
  }
}

// Shorter syntax
class Invoice {
  constructor(
    public client: string,
    private details: string,
    public amount: number
  ) {}
}

模块

要使用 import 和 export,需要在 tsconfig.json 中设置 "module": "es2015" 或 "module": "ES6",并在 script 标签上加上 type="module":

<script type="module" src="app.js"></script>

接口

接口用来定义对象的结构,包括属性和方法。主要特点:

  1. 接口是一种类型,和 TypeScript 中的其他类型(如 string、number)地位相同。
  2. 接口可以描述对象的形状,包含多个属性和方法。
  3. 类可以实现某个接口,此时必须实现该接口的所有属性和方法。
  4. 接口可以继承其他接口,从而派生出更具体的接口。
interface IsPerson {
  name: string;
  age: number;
  speak(a: string): void;
  spend(a: number): number;
}

const me: IsPerson = {
  name: 'shaun',
  age: 30,
  speak(text: string): void {
    console.log(text);
  },
  spend(amount: number): number {
    console.log('I spent', amount);
    return amount;
  },
};

let someone: IsPerson; // The type of 'someone' is IsPerson.

const greetPerson = (person: IsPerson) => {
  console.log('hello', person.name);
};

在类中使用接口

可以让类实现接口,以确保它具备接口要求的结构:

import { HasFormatter } from '../interfaces/HasFormatter.js';

export class Invoice implements HasFormatter {
  constructor(
    readonly client: string,
    private details: string,
    public amount: number
  ) {}

  format() {
    return `${this.client} owes $${this.amount} for ${this.details}`;
  }
}

泛型

泛型让你写出的代码既能适配多种数据类型,又能保持类型安全。要点:

  1. 泛型可以用在函数、类、接口和类型别名上。
  2. 定义泛型时声明类型参数,使用时再传入具体类型。
  3. 可以给类型参数添加约束,限制允许的类型范围。
  4. 可以为类型参数指定默认类型。
const addUID = <T>(obj: T) => {
  let uid = Math.floor(Math.random() * 100);
  return { ...obj, uid };
};

let docOne = addUID({ name: 'yoshi', age: 40 });

console.log(docOne);

const addUID = <T extends object>(obj: T) => {
  let uid = Math.floor(Math.random() * 100);
  return { ...obj, uid };
};

const addUID = <T extends { name: string }>(obj: T) => {
  let uid = Math.floor(Math.random() * 100);
  return { ...obj, uid };
};

枚举

枚举用来定义一组具名的数值,可以表示一组相关的常量。主要特性:

  1. 枚举为一组相关常量提供了具名的值。
  2. 枚举可以给数值赋予更易读的名字。
  3. 枚举可以用来表示一组相关的选项。
enum ResourceType {
  BOOK,
  AUTHOR,
  FILM,
  DIRECTOR,
  PERSON,
}

interface Resource<T> {
  uid: number;
  resourceType: ResourceType;
  data: T;
}

const docOne: Resource<object> = {
  uid: 1,
  resourceType: ResourceType.BOOK,
  data: { title: 'name of the wind' },
};

const docTwo: Resource<object> = {
  uid: 10,
  resourceType: ResourceType.PERSON,
  data: { name: 'yoshi' },
};

元组

元组是一种特殊的数组类型,允许你指定每个元素的类型和顺序。要点:

  1. 元组的长度固定,元素顺序也是确定的。
  2. 元组中的每个元素可以是不同的类型。
  3. 访问元组元素时按位置来取。
let tup: [string, number, boolean] = ['ryu', 25, true];
tup[0] = 'ken'; // Values can be reassigned, but types cannot be changed.
// tup[0] = 30; // Error

let student: [string, number];
student = ['chun-li', 223423];
// student = [223423, 'chun-li']; // Error

let values: [string, string, number];
values = [tofrom.value, details.value, amount.valueAsNumber];
if (type.value === 'invoice') {
  doc = new Invoice(...values);
} else {
  doc = new Payment(...values);
}

接口 vs 类型别名

// Using an interface to define an object's structure
interface Car {
  brand: string;
  model: string;
  year: number;
}

// Using a type alias to define the same object structure
type CarType = {
  brand: string;
  model: string;
  year: number;
};

// Creating an object that conforms to the interface
const myCar: Car = {
  brand: 'Toyota',
  model: 'Camry',
  year: 2022,
};

// Creating an object that conforms to the type alias
const myCarType: CarType = {
  brand: 'Honda',
  model: 'Civic',
  year: 2023,
};

// Both interfaces and type aliases support optional properties
interface Person {
  name: string;
  age?: number;
}

type PersonType = {
  name: string;
  age?: number;
};

// Implementing an interface requires adhering to its structure
class Student implements Person {
  constructor(public name: string, public age: number) {}
}

// Implementing a type alias also requires adhering to its structure
class Teacher implements PersonType {
  constructor(public name: string, public age: number) {}
}