類別方法
例:
class Greeter {
greet(name: string) {
console.log(name + ", hi!");
}
}
new Greeter().greet("tt"); // 正確
new Greeter().greet(); // 報錯:應有1個引數。
建構函式的例子:
class Greeted {
constructor(message: string) {
console.log("Hi, " + message + "!");
}
}
new Greeted("abc"); // 正確
new Greeted(); // 報錯:應有1個引數
類別屬性
類別屬性使用與介面宣告的語法相同:在屬性名稱後面緊接著選擇的型別註記。
TypeScript 不會從建構函式中,推斷類別中可能存在的成員。
例:
class FieldTrip {
// 類別屬性:明確宣告 destination 為 string
destination: string;
constructor(destination: string) {
this.destination = destination; // 正確
// 報錯:沒有 abc 這個屬性
this.abc = destination;
}
}
const trip = new FieldTrip("plane");
trip.destination; // 正確
trip.abc; // 報錯
函式屬性
先瞭解有 JavaScript 當中的類別函式,有兩種寫法(方法和屬性)。
語法一:
class WithMethod {
myMethod(){
console.log("abc");
}
}
// 判斷結果是 true,所有的類別實體都使用相同的函式定義。
new WithMethod().myMethod === new WithMethod().myMethod;
語法二:
class WithProperty {
myProperty: () => {
console.log("abc");
}
}
// 判斷結果是 false,這將會為每個類別實體建立一個新函式
new WithProperty().myProperty === new WithProperty().myProperty;
可以使用與類別方法和獨立函式相同的語法:
class abcde {
takesParameters = (input: boolean) => input ? "Yes" : "No";
}
const instance = new abcde();
instance.takesParameters(true); // 正確
// 報錯:型別 number 的引數,不可指派給型別 boolean 的參數
instance.takesParameters(123);
初始化檢查
開啟嚴格(建議開啟 strictPropertyInitialization)的編譯器設定的話,可以讓 TypeScript 檢查建構函式中,每個屬性宣告的型別,如下情況:
class WithValue {
immediate = 0; // 正確
later: number; // 正確(因為有在constructor中設定初始值)
mayBeundefined: number | undefined; // 正確(允許 undefined)
// 報錯:屬性 unused 沒有設定初始值,在 constructor 中也沒有明確給定資料
unused: number;
constructor() {
this.later = 1;
}
}
明確指定的屬性
如果確保一個屬性不需要套用嚴格的初始化檢查的話,可以在屬性名稱之後加一個驚嘆號,則可關閉檢查。
這樣做會向 TypeScript 斷言,該屬性將在第一次使用之前,會被指派一個非 undefined 的資料。
例(避免使用,建議應用 constructor):
class ActivitiesQueue {
pending!: string[]; // 正確,若沒有加驚嘆號,就會報錯。
initialize(pending: string[]) {
this.pending = pending;
}
next() {
return this.pending.pop();
}
}
const abc = new ActivitiesQueue();
abc.initialize(["a", "b", "c"]);
abc.next();
可選擇的屬性
可以在類別屬性名稱之後,加上問號,來代表該屬性是可選擇的:
class MissingInitializer {
property?: string;
}
// 正確
let a = new MissingInitializer().property?.length;
console.log(a); // 會印出 undefined
// 報錯:Object is possibly 'undefined'.
new MissingInitializer().property.length;
唯讀屬性
類別可以在宣告名稱之前加上 readonly 關鍵字來將屬性宣告成唯讀,宣告為 readonly 的屬性,只能在宣告它們的地方或在建構函式中,指派初始資料。例:
class Quote {
readonly text: string;
constructor(text: string) {
// 在建構函式中,設定初始資料,是可以的。
this.text = "";
}
emphasize() {
// 報錯:因為 text 是唯讀屬性。
this.text += "a";
}
}
const quote = new Quote("aaa");
// 以下這行報錯,因為 text 是唯讀屬性,無法設定資料。
Quote.text = "hihi";
另外一種情況,宣告為具有基本初始資料的唯讀屬性與其它屬性相比,它們可能被推斷為經過資料窄化的字面型別。例:
class RandomQuote {
readonly a: string = "aaa";
readonly b = "bbb";
constructor() {
if(Math.random() > 0.5){
this.a = "aaa change"; // 正確
// 以下這行報錯:型別 "bbb change" 不可指派給型別 "bbb"
this.b = "bbb change";
}
}
}
const quote = new RandomQuote();
quote.a; // 型別:string
quote.b; // 型別:"bbb"
作為型別的類別
下例中,有一個 teacher 變數,型別為 Teacher 類別:
class Teacher {
sayHello() {
console.log("Hello");
}
}
let teacher: Teacher;
teacher = new Teacher(); // 正確
// 報錯:型別 string 不可指派給型別 Teacher。
teacher = "wow";
另一個例子:withSchoolBus 接受一個 SchoolBus 型別的參數。透過任何具有 () => string[] 型別的 getAbilities 屬性來達成,例:
class SchoolBus {
getAbilities() {
return ["a", "b"];
}
}
// 參數要求類別型別,較少見
function withSchoolBus(bus: SchoolBus) {
console.log(bus.getAbilities());
}
withSchoolBus(new SchoolBus()); // 正確
// 正確
withSchoolBus({
getAbilities: () => ["c"]
});
// 以下會報錯:型別 number 不可指派給型別 string[]
withSchoolBus({
getAbilities: () => 123
});
類別和介面
TypeScript 允許在類別名稱之後加上 implements 關鍵字,後面緊接著所遵循的介面,代表介面中的每一個部分都要被實作。
例:
interface Learner {
name: string;
study(hours: number): void;
}
class Student implements Learner {
name: string;
constructor(name: string) {
this.name = name;
}
study(hours: number) {
console.log("abc");
}
}
class Slacker implements Learner {
// 報錯:類別 Slacker 不正確地實作介面 Learner。
// 型別 Slacker 缺少屬性 study。
name = "Rocky";
}
實作介面只是一種安全檢查。它不會將介面當中的任何成員,複製到類別中。
多個實作的介面
在 TypeScript 中,允許將類別宣告為實作多個介面,以逗號區隔。
例:
interface Graded {
grades: number[];
}
interface Reporter {
report: () => string;
}
class ReportCard implements Graded, Reporter {
grades: number[];
constructor(grades: number[]) {
this.grades = grades;
}
report() {
return this.grades.join(", ");
}
}
// 報錯:類別 Empty 不正確地實作介面 Graded。
// 型別 Empty 缺少屬性 grades。
// 報錯:類別 Empty 不正確地實作介面 Reporter。
// 型別 Empty 缺少屬性 report。
class Empty implements Graded, Reporter {
}
在開發過程中,可能有某些介面的定義,會使得某一個類別不可能同時實作這兩者,將導致該類別至少出現一個型別錯誤,所以需留意。
擴充(繼承)類別
在基本類別(base class)上宣告的任何方法或屬性,都可在子類別(也稱做衍生類別)使用。
例:
class Teacher {
teach() {
console.log("teach");
}
}
// Teacher 是基本類別(base class),StudentTeacher 是衍生類別或稱做子類別
class StudentTeacher extends Teacher {
learn() {
console.log("learn");
}
}
const teacher = new StudentTeacher();
teacher.teach(); // 正確(在 base class 有定義)
teacher.learn(); // 正確(在 subclass 有定義)
// 報錯:型別 StudentTeacher 沒有 other 屬性
teacher.other();
指派性的擴充
例:
class Lesson {
subject: string;
constructor(subject: string) {
this.subject = subject;
}
}
class OnlineLesson extends Lesson {
url: string;
constructor(subject: string, url: string) {
super(subject); // 會執行基本類別(父類別)
this.url = url;
}
}
let lesson: Lesson;
lesson = new Lesson("coding"); // 正確
lesson = new OnlineLesson("coding", "abc"); // 正確
let online: OnlineLesson;
online = new OnlineLesson("coding", "def"); // 正確
// 報錯:型別 Lesson 缺少 url 屬性,但型別 OnlineLesson 必須有 url 屬性
online = new Lesson("coding");
覆寫建構函式
與原生 JavaScript 一樣,TypeScript 不需要子類別來定義自己的建構函式。沒有自己子類別的建構函式,會隱函使用基本類別(父類別)的建構函式。
在 JavaScript 中,如果子類別確實宣告自己的建構函式,這麼必須透過 super 關鍵字呼叫其基本類別的建構函式。
例:
class GradeAnnouncer {
message: string;
constructor(grade: number) {
this.message = grade >= 65 ? "a" : "b";
}
}
class PassingAnnouncer extends GradeAnnouncer {
constructor() {
super(100);
}
}
class FailingAnnouncer extends GradeAnnouncer {
// 報錯:衍生類別的建構函式必須執行 super 函式
constructor() {
}
}
根據 JavaScript 規則,子類別的建構函式必須在存取 this 或 super 之前,呼叫基本建構函式。如果 TypeScript 在 super() 之前看到 this 或 super 被存取,系統將報告型別錯誤。例:
class GradesTally {
grades: number[] = [];
aaa(...grades: number[]) {
this.grades.push(...grades);
return this.grades.length;
}
}
class ContinuedGradesTally extends GradesTally {
constructor(previousGrades: number[]) {
this.grades = [...previousGrades];
// 上面這行報錯:因為必須先呼叫 super,才能存取衍生類別中建構函式的 this
super();
}
}
覆寫方法
子類別可以重新宣告與基本類別相同名稱的新方法。只要子類別上的方法可以指派給基本類別。
例:
class GradeCounter {
countGrades(grades: string[], letter: string) {
// 回傳 number
return grades.filter(grade => grade === letter).length;
}
}
class FailureCounter extends GradeCounter {
// 此覆寫是ok的
countGrades(grades: string[]) {
return super.countGrades(grades, "F");
}
}
class Any FailureChecker extends GradeCounter {
// 此覆寫會失敗,因為回傳的型別不一致
countGrades(grades: string[]) {
// 回傳 boolean
return super.countGrades(grades, "F") !== 0;
}
}
const counter: GradeCounter = new AnyFailureChecker();
// 預期型別:number
// 實際型別:boolean
const count = counter.countGrades(["A", "C", "F"]);
覆寫屬性
子類別也可以用相同的名稱,明確重新宣告其基本類別的屬性,只要新型別可以指派給基本類別上的型別。
在以下範例中,基本類別 Assignment 將 grade 宣告為 number | undefined,而子類別 GradedAssignment 將其宣告為必須存在的 number:
class Assignment {
// number | undefined
grade?: number;
}
class GradedAssignment extends Assignment {
// 覆寫成功,此型別為 number
grade: number;
constructor(grade: number) {
super();
this.grade = grade;
}
}
下例是錯誤的:
class NumericGrade {
value = 0;
}
class VagueGrade extends NumericGrade {
// 以下覆寫失敗,因為可能會是 string 型別
value = Math.random() > 0.5 ? 1 : "aaa";
}
const instance: NumericGrade = new VagueGrade();
// 預期型別:number
// 實際型別:number | string
instance.value;
抽象類別
在專案中,有時會建立一個本身不宣告方法的實作,期望提供一個基本類別作為子類別實作的參考。將類別標記為抽象,可將 TypeScript 的 abstract 關鍵字,加在類別名稱或任何打算抽象的方法之前。
以下範例中 School 類別及其 getStudentTypes 方法被標記為 abstract。因此,它的子類別 Preschool 和 Absence 應該實作 getStudentTypes:
abstract class School {
readonly name: string;
constructor(name: string) {
this.name = name;
}
abstract getStudentTypes(): string[];
}
class Preschool extends School {
getStudentTypes() {
return ["abc"];
}
}
// 報錯:類別 Absence 未實作從類別 School 繼承而來的 getStudentTypes
class Absence extends School {
}
抽象(abstract)類別不能直接實體化,因為沒有對其實作,僅只假設可能確實存在某些方法的定義。只有非抽象類別能實體化。例:
let school: School;
school = new Preschool("aaa"); // 正確
// 報錯:無法建立抽象類別的執行個體
school = new School("bbb");
成員的可見性
包括 JavaScript,若類別成員名稱,前面有#做開頭,則是標記為 private 類別成員。private 類別成員只能由該類別的實體做存取。如果類別之外的程式碼試圖存取的話,會報錯。
TypeScript 的成員可見性,是透過增加以下關鍵字來達成:
- public:允許任何人、任何地方存取。
- protected:只允許類別本身及其子類別存取。
- private:只允許類別本身存取。
以上關鍵字只存在於型別系統中。當程式碼編譯為 JavaScript 時,都會被刪除。
例:
class Base {
isPublicImplicit = 0; // 這會隱含為 public
public isPublicExplicit = 1;
protected isProtected = 2;
private isPrivate = 3;
#truePrivate = 4; // 前面有 # 字號,代表真正的私有屬性
}
class Subclass extends Base {
examples() {
this.isPublicImplicit; // 允許
this.isPublicExplicit; // 允許
this.isProtected; // 允許
// 報錯:isPrivate 是私有屬性,只可從類別 Base 中存取
this.isPrivate;
// 報錯:因為屬性 #truePrivate 具有私人識別碼,所以無法在類別 Base 外存取這個屬性
this.#truePrivate;
}
}
new Subclass().isPublicImplicit; // 允許
new Subclass().isPublicExplicit; // 允許
// 報錯:isProtected 是受保護的屬性,只可從類別 Base 及其子類別中存取。
new Subclass().isProtected;
// 報錯:isPrivate 是私有屬性,只可從類別 Base 中存取。
new Subclass().isPrivate;
註:# 字號是表示私有欄位,它在執行時 JavaScript 中才是真正的私有,因為 # 字號是 JavaScript 中本來就有的語法。
可見性修飾符號可以和 readonly 一起使用。要將成員宣告為 readonly 並且為可見的,那麼可見性會是第一順位被考慮的。例:
class TwoKeywords {
private readonly name: string;
constructor() {
this.name = "aaa"; // 允許
}
log() {
console.log(this.name); // 允許
}
}
const two = new TwoKeywords();
// 報錯:name 是私有屬性,只可從類別 TwoKeywords 中存取。
// 報錯:name 是唯讀,所以無法將資料指派給 name。
two.name = "ttt";
留意:TypeScript 不允許將可見性關鍵字與 JavaScript 的新 # 私有欄位混合使用。
靜態(static)欄位修飾符號
JavaScript 允許在類別本身,使用 static 關鍵字宣告成員。例:
class Question {
protected static readonly answer: "bash";
protected static readonly prompt = "aaa";
guess() {
const answer = Question.prompt; // 允許
}
}
// 以下會報錯:answer 是 protected,只可從類別 Question 及其子類別存取
Question.answer;
發佈留言
很抱歉,必須登入網站才能發佈留言。