返回文章列表

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) {}
}