TypeScript的規範較為嚴謹,可以藉由定義型別來避開原本可能要打更多程式碼才能處理的錯誤。
// example
function example( a: number | string, b: number | string ) {
// ...function content
}
// example
let example: "hello" ;
let example_literal_union: "hello" | "world"
string
, number
, boolean
switch...case
// example
enum Role{ ADMIN, READ_ONLY, AUTHOR, CUSTOM = 100 }
// example
let unknownUserInput: unknown
let anyUserInput: any
let userName: string
// 假設使用者輸入的是一般字串
userName = unknownUserInput // 會出現錯誤
userName = anyUserInput // 不會出現錯誤
// 前者需要經過一層型別檢查才能賦值給其他型別的變數
if ( typeof unknownUserInput === "string" ) {
userName = unknownUserInput
}
部分預設型別的英文為小寫,例如string
或number
,這是 TypeScript 自身追加的規範。
為的是和 JS 原生的String
和Number
做區分,兩者在函式庫上的支援亦有些許差異。
原則上 JS 屬於 弱型別語言,故在撰寫 TS 的時候,不應該 繼續沿用 JS 原生的型別,因為原生型別並沒有強制性,而只是一種 標註。
const num_1: number ; // 在變數後打上冒號並追加想要的型別
const str_1: string ;
// 通常只會使用在未定義初值,只定義型態的時候
// 因為 tsc 編譯時可以自動抓取你定義的初值的型態,所以不需要手動再給一次型別
// 在參數後打上冒號並追加想要的型別
// 在參數後加上問號代表該參數非必要
function add( num1: number, num2: number, num3?: number ) {
if ( num3 ) {
return num1 + num2 + num3
}
return num1 + num2 ;
}
const num_1 = 5;
const num_2 = 2.8;
const result = add( num_1, num_2 );
console.log( result )
// 當參數需要支援多種型別時,則使用 Union
function combine( left: number | string, right: number | string ) {
// 利用 if...else 限縮編譯器接收到的實際型別
if ( typeof left === "number" && typeof right === "number" ) {
return left + right;
}
else {
return left.toString() + right.toString()
}
}
let myHobbies: string[];
myHobbies = ["Sports"]
// tsc 在處理時,會自動將所有成員的型別認定為您預定義的型別
// 可以保證你在使用相關內建函式時不會出現錯誤
const person: {
name: string;
age: number;
}
person = {
name: "YumekuiiNight",
age: 30
}
/**
* 若預定義的方式同前面一樣只用 :object 的話,
* 會出現 tsc 編譯時無法知道內部成員的情況,
* 甚至有可能造成額外的錯誤
*/
// 以下則是巢狀物件的定義方法
const _nested_Product: {
id: string;
price: number;
tags: string[];
details: {
title: string;
description: string;
}
}
_nested_Product = {
id: 'abc1',
price: 12.99,
tags: ['great-offer', 'hot-and-new'],
details: {
title: 'Red Carpet',
description: 'A great carpet - almost brand-new!'
}
}
const myTuple: [number, string];
myTuple = [2, "hello"]
/**
* 因為 Tuple 在編譯後仍然是用 Array 去實現的,
* 所以少部分內建函式在不應該能運作的時候仍然是可以運作的
*/
myTuple.push("world") // 不會出現錯誤
myTuple = [0, "init", "something else"] // 會出現錯誤
// 類似 C 的 struct
// 將 Union, Literal 或 object 型別的變數再做一次包裝
type combinable = number | string ;
type conversionDescriptor = "as-text" | "as-number"
type User = { name: string, age: number }
// 也可以包裝一般的型別
type helloNumber = 0
type helloString = "HelloWorld"
type helloBoolean = true
// 使用方法
let myNumber: helloNumber = 0 // 若給 0 以外的值會出現錯誤
function combine(
left: combinable,
right: conbinable,
resultCase: conversionDescriptor,
user: User
) {
// ...function content
}
function example( a, b ) {
// content...
return result
}
function example( a: number, b: number ): number {
// content...
return result
}
function example( param: string ): void {
// content
// no return
}
function example( param: string ): never {
// content
// should not have return
}
// 代表任何函式
let listener_a: Function;
// 代表需要兩個參數,並且回傳型別為 number 之函式
let listener_b: ( a: number, b: number ) => number;
// 參數的名稱並不會造成影響,參數的個數可以向下兼容
listener_b = param => {
// 內容...
}
tsc --init
tsconfig.json
,包含所有關於 typescript 編譯器的詳細設定
tsc <filename> --watch
// or
tsc <filename> -w
// 有先進行 Initialize 的資料夾,可以直接監看該資料夾下的所有檔案變化
tsc --watch
// or
tsc -w
// tsconfig.json
{
compilerOptions: {
// ...
// ..
// .
},
"exclude": [],
"include": [],
// 單一檔案導入
"files": []
}
// tsconfig.json
{
"compilerOptions": {
/* Projects */
// "incremental": true,
// "composite": true,
// "tsBuildInfoFile": "./.tsbuildinfo",
// "disableSolutionSearching": true,
// "disableReferencedProjectLoad": true,
/* Language and Environment */
"target": "es2016",
// "lib": [],
// "jsx": "preserve",
// "experimentalDecorators": true,
// "emitDecoratorMetadata": true,
// "jsxFactory": "",
// "jsxFragmentFactory": "",
// "jsxImportSource": "",
// "reactNamespace": "",
// "noLib": true,
// "useDefineForClassFields": true,
// "moduleDetection": "auto",
/* Modules */
"module": "commonjs",
// "rootDir": "./",
// "moduleResolution": "node",
// "baseUrl": "./",
// "paths": {},
// "rootDirs": [],
// "typeRoots": [],
// "types": [],
// "allowUmdGlobalAccess": true,
// "moduleSuffixes": [],
// "resolveJsonModule": true,
// "noResolve": true,
/* JavaScript Support */
// "allowJs": true,
// "checkJs": true,
// "maxNodeModuleJsDepth": 1,
/* Emit */
// "declaration": true,
// "declarationMap": true,
// "emitDeclarationOnly": true,
// "sourceMap": true,
// "outFile": "./",
// "outDir": "./",
// "removeComments": true,
// "noEmit": true,
// "importHelpers": true,
// "importsNotUsedAsValues": "remove",
// "downlevelIteration": true,
// "sourceRoot": "",
// "mapRoot": "",
// "inlineSourceMap": true,
// "inlineSources": true,
// "emitBOM": true,
// "newLine": "crlf",
// "stripInternal": true,
// "noEmitHelpers": true,
// "noEmitOnError": true,
// "preserveConstEnums": true,
// "declarationDir": "./",
// "preserveValueImports": true,
/* Interop Constraints */
// "isolatedModules": true,
// "allowSyntheticDefaultImports": true,
"esModuleInterop": true,
// "preserveSymlinks": true,
"forceConsistentCasingInFileNames": true,
/* Type Checking */
"strict": true,
// "noImplicitAny": true,
// "strictNullChecks": true,
// "strictFunctionTypes": true,
// "strictBindCallApply": true,
// "strictPropertyInitialization": true,
// "noImplicitThis": true,
// "useUnknownInCatchVariables": true,
// "alwaysStrict": true,
// "noUnusedLocals": true,
// "noUnusedParameters": true,
// "exactOptionalPropertyTypes": true,
// "noImplicitReturns": true,
// "noFallthroughCasesInSwitch": true,
// "noUncheckedIndexedAccess": true,
// "noImplicitOverride": true,
// "noPropertyAccessFromIndexSignature": true,
// "allowUnusedLabels": true,
// "allowUnreachableCode": true,
/* Completeness */
// "skipDefaultLibCheck": true,
"skipLibCheck": true
}
}
2022/8/1 目前只會註記常用的設定的解釋,以後有機會會陸續補上。
target
lib
document
allowJS
*.js
結尾的檔案checkJS
*.js
中的潛在錯誤jsx
sourceMap
*.js.map
檔outDir
*.js
檔案的存放位置rootDir
*.ts
檔removeComments
noEmit
noEmitOnError
downlevelIteration
strict
告訴編譯器將要使用 strict mode 來編譯出 *.js
效果等同於將 strict mode 相關的所有 options 設為 true
noImplicitAny
any
型別
// 隱式宣告 param 為 any 型別
function example( param ) { // 會在這行出現錯誤
console.log( param )
}
example("Hello World!")
strictNullChecks
undefined
或 null
const button = document.querySelector("button") // 會出現錯誤
// 可以在宣告式結尾加上一個驚嘆號
// 代表略過 null check
const div = document.querySelector("div")!
// 以下是比較嚴謹的作法
if ( button ) {
button.addEventListener("click", click_handler)
}
strictBindCallApply
alwaysStrict
*.js
開頭加上 use strict
來保證編譯出的檔案正在使用 strict modenoUnusedLocals
noUnusedParameters
noImplicitReturns
function example( value ) {
if ( value === 0 ) {
return 1
}
// 需要另外加上 return 關鍵字
// 注意函式結束點不一定是函式結尾
return
}
allowUnusedLabels
function verifyAge(age: number) {
if (age > 18) {
verified: true;
}
}
allowUnreachableCode
class Department {
/* 成員變數宣告 */
name: string
/* 類別建構子 */
constructor( n: string ) {
this.name = n
}
/* 成員函式 */
describe( this: Department ) {
console.log( "Department" + this.name )
}
}
// after Department class defined
// 當呼叫 new 關鍵字時,TypeScript 會自動找到 class 的 constructor 執行
const accounting = new Department( "Accounting" );
// after Department class defined
const accounting = new Department( "Accounting" );
accounting.describe(); // 預期輸出: Department: Accounting
const accountingCopy = { describe: accounting.describe }
/**
* TypeScript 會因為參數中的 this 關鍵字的原因
* 知道這個函式會使用 this 的某個 property
* 而因為 accountingCopy.describe() 指向的 this
* 會被重新定義到 accountingCopy 本身
* 故 TypeScript 這時找不到 this.name
* 便會在這邊報錯
*/
accountingCopy.describe(); // 會出現錯誤
// rewrite Department
class Department {
// 預設若不帶 public / private 關鍵字時,成員都會是 public 公用的
public name: string // 公用變數
private employees: string[] = [] // 私用變數
constructor( n: string ) {
this.name = n
}
describe( this: Department ) {
console.log( "Department" + this.name )
}
addEmployee( employee: string ) {
this.employees.push( employee )
}
printEmployeeInformation() {
console.log( this.employees.length )
console.log( this.employees )
}
}
const accounting = new Department( "Accounting" )
/**
* 私用成員外部不可存取
* 雖然是這樣說,但是 TypeScript 是基於 JavaScript 設計的語言
* 而 ECMAScript 一直到最近才有所謂公用私用的概念出現
* 所以這樣的 code,雖然一定會在 TypeScript 編譯器中報錯
* 但是如果意外編譯成功,編譯出的 JavaScript 還是有可能可以這樣執行的
*/
accounting.employees[ 2 ] = "Anna" // 會出現錯誤
// 公用成員外部可以存取
accounting.name = "NEW NAME"
// 私用成員應該透過內部函式做存取或修改
accounting.addEmployee( "John" )
accounting.addEmployee( "Kao" )
accounting.printEmployeeInformation()
// 預期輸出
// 2
// ["John", "Kao"]
// rewrite Department
class Department {
// private id: string
// private name: string
// 可以直接在 constructor 定義成員變數
constructor( private id: string, private name: string ) {
// this.id = id
// this.name = n
}
describe( this: Department ) {
console.log( `Department (${ this.id }): this.name` )
}
}
const accounting = new Department( "b1", "Accounting" )
accounting.describe()
// 預期輸出
// Department (b1): Accounting
// rewrite Department
class Department {
// private readonly id: string
// private name: string
// readonly 關鍵字只能用在 class 或 interface 中
// 用途和 const 類似,都是禁止 reassign 的動作
constructor( private readonly id: string, private name: string ) {
// this.id = id
// this.name = n
}
describe( this: Department ) {
console.log( `Department (${ this.id }): this.name` )
}
}
// rewrite Department
class Department {
// private readonly id: string
// private name: string
constructor( private readonly id: string, private name: string ) {
// this.id = id
// this.name = n
}
describe( this: Department ) {
console.log( `Department (${ this.id }): this.name` )
}
}
class ITDepartment extends Department {
admins: string[]
// 子類別在沒有寫任何內容的情況下
// 預設會複製所有父類別的可繼承的成員變數或成員函式
constructor( id: string, admins: string[] ) {
// super 關鍵字只能用在有繼承父類別的類別
// 會呼叫父類別的 this 作使用
super( id, "IT" )
// 在子類別中,如果需要呼叫 this 關鍵字,順序都必須在 super 關鍵字之後
this.admins = admins
}
}
const it = new ITDepartment( "d1", ["Max"] )
console.log( it )
/**
* 預期輸出
* ITDepartment {
* id: 'd1',
* name: "IT",
* employees: [],
* admins: ["Max"]
* }
// rewrite Department & ITDepartment
class Department {
private readonly id: string
private name: string
// 若希望父類別傳遞成員給子類別,但又不希望使用 public 導致外部也能存取時
// 則使用 protected
protected employees: string[]
constructor( id: string, name: string ) {
this.id = id
this.name = name
}
describe( this: Department ) {
console.log( `Department (${ this.id }): ${ this.name }` )
}
addEmployees( employee: string ) {
this.employees.push("John")
}
}
class ITDepartment extends Department {
admins: string[]
constructor( id: string, admins: string[] ) {
super( id, "IT" )
this.admins = admins
}
// 這邊的 addEmployees 會覆寫父類別的 addEmployees
addEmployees( employee: string ) {
if ( !this.admins.includes( employee ) ) {
this.employees.push( employee )
}
}
}
Public | Protected | Private | |
---|---|---|---|
內部 | 可存取 | 可存取 | 可存取 |
外部 | 可存取 | 不可存取 | 不可存取 |
子類別 | 可存取 | 可存取 | 不可存取 |
// rewrite Department
// 抽象類別需要在 class 前面加上關鍵字 abstract
// 另外抽象類別只能被其他類別繼承,不得使用該類別進行實例化
abstract class Department {
private readonly id: string
private name: string
constructor( id: string, name: string ) {
this.id = id
this.name = name
}
/**
* 這代表強制任何繼承該類別的子類別
* 必須要實作出 describe 函式
* abstract 關鍵字也可以加在變數前
*/
abstract describe( this: Department ): void;
}
class ITDepartment extends Department {
describe() {
console.log( "${ this.name } Department - ID: ${ this.id }" )
}
}
const it = new ITDepartment( "d1", "IT" )
it.describe() // 預期輸出: IT Department - ID: d1
// rewrite Department
class Department {
private readonly id: string
private name: string
private lastEmployee
// 若希望父類別傳遞成員給子類別,但又不希望使用 public 導致外部也能存取時
// 則使用 protected
protected employees: string[]
constructor( id: string, name: string ) {
this.id = id
this.name = name
}
get latestEmployee() {
if ( this.lastEmployee ) {
return this.lastEmployee
}
throw new Error("No employee found.")
}
set latestEMployee( employee ) {
if ( !employee ) {
throw new Error("Please enter a valid value.")
}
this.addEmployee( employee )
}
describe( this: Department ) {
console.log( `Department (${ this.id }): ${ this.name }` )
}
addEmployees( employee: string ) {
this.employees.push("John")
this.lastEmployee = employee
}
}
coonst accounting = new Department( "b1", "Accounting" )
accounting.addEmployess("Max")
accounting.addEmployess("John")
// getter 和 setter 都是在發生相關動作時會被呼叫
// 所以在 access 的時候,不需要像函式一樣加上括號
console.log( accounting.latestEmployee ) // 預期輸出: ["Max", "John"]
accounting.lastestEmployee = "Jack" // 預期輸出: ["Max", "John", "Jack"]
// rewrite Department
class Department {
private readonly id: string
private name: string
private employees: string[] = []
// 加上 static 後的變數或函式,可以不需要實例化,直接透過 class 來 access
static fiscalYear: number = 2022
constructor( id: string, name: string ) {
this.id = id
this.name = name
}
static createEmployee( name: string ) {
return { "name": name }
}
describe( this: Department ) {
console.log( `Department (${ this.id }): this.name` )
}
addEmployee( employee: { name: string } ) {
this.employees.push( employee )
}
}
const accounting = new Department( "b1", "Accounting" )
const newEmployee = Department.createEmployee( "Jack" )
accounting.addEmployee( newEmployee ) // [ { name: "Jack" } ]
console.log( Department.fiscalYear ) // 預期輸出: 2022
// rewrite Department
/**
* 建構子可以被設定為 private
* 多半使用在一種被稱為單例設計模式的寫法
* 藉由將建構子私有化,並且將產生出的 instance 存在 class 內
* 再透過 static 方法將 instance 傳出去
* 來保證一個類別必定只會被實作一次
*/
class Department {
private readonly id: string
private name: string
private static instance: Department
private constructor( id: string, name: string ) {
this.id = id
this.name = name
}
static getInstance() {
if ( this.instance ) {
return this.instance
}
this.instance = new Department( "b1", "Accounting" )
return this.instance
}
}
const account = Department.getInstance()
/**
* interface 中不可包含變數值的宣告
* 亦不可包含函式的實作
* 形式上比較接近 type
* 但 interface 只能包裝 object
*/
interface Person {
name: string;
age: number;
greet( pharse: string ): void;
}
// 一般使用方法
// 幾乎和 type 沒有差別
let user_1: Person;
user_1 = {
name: 'Max',
age: 30,
greet( pharse: string ) {
console.log( pharse + ' ' + this.name )
}
}
user_1.greet( "Hello, I am " )
interface Greetable {
// interface 中無法使用 public / protected / private
// 但可以使用 readonly
readonly name: string;
// interface 中也可以存在有非必要參數
// 非必要參數被繼承到 class 時,則不一定需要被宣告或實作
greet( pharse: string ): void;
}
/**
* 和 abstract class 類似
* class 可以以 interface 為基底來實作
* 一樣會強制該 class 需要實作或宣告 interface 中所有成員
* 另外和 abstract class 不同的是,一個 class 可以同時實作多個 interface
*/
class Person implements Greetable {
// 父介面定義為 readonly 的屬性
// 將會在 class 中繼續保留唯獨的特性
// 但可以不用再添加一次關鍵字
name: string;
// 可以根據需要自行增加變數或函式
age: number = 30
constructor( n: string ) {
this.name = n
}
greet( pharse: string ) {
console.log( pharse + ' ' + this.name )
}
}
// 使用 interface 作為型別的變數
// 可以存放實作該 interface 的 class 的實例
let user_1: Greetable
user_1 = new Person( "Max" )
user_1.greet( "Hello, I am " )
// 您可以選擇用 interface 互相繼承的方法
// 也可以選擇讓單一 class 實作多個 interface
// 兩這差異不大
interface Named {
readonly name: string
}
interface Greetable extends Named {
greet( pharse: string ): void;
}
class Person implememts Greetable {
name: string
age: number = 30
greet( pharse: string ) {
console.log( pharse + ' ' + this.name )
}
}
// 需要有兩個 number 的變數,並回傳 number 型別
interface mathFunction {
( a: number, b: number ) => number;
}
let add: mathFunction
add = ( left: number, right: number ) => {
return left + right;
}
type Admin = {
name: string;
privileges: string[];
}
type Employee = {
name: string;
startDate: Date;
}
type ElevatedEmployee = Admin & Employee
// 等同於以下
// interface ElevatedEmployee extends Admin, Employee
// 為兩種 object 的增集
// 但 type 的寫法較為簡潔
const el: ElevatedEmployee = {
name: "Max",
privileges: [ "createServer" ]
startDate: new Date()
}
type Combinable = string | number
type Numeric = number | boolean
// typeof Universal === number
type Universal = Combinable & Numeric
type Admin = {
name: string;
privileges: string[];
}
type Employee = {
name: string;
startDate: Date;
}
type UnknownEmployee = Employee | Admin
function printEmployeeInformation( emp: UnknownEmployee ) {
// 因為 Employee 和 Admin 都不是原生型別
// 所以這裡無法用 typeof 來確認
// 要利用是否有該成員變數來判斷
if ( "privileges" in emp ) {
console.log( "Privileges: " + emp.privileges )
}
if ( "startDate" in emp ) {
console.log( "Join Date: " + emp.startDate )
}
}
class Car {
drive() {
console.log("Drining......")
}
}
class Truck {
drive() {
console.log("Driving a truck......")
}
loadCargo( amount: number ) {
console.log("Loading Cargo..." + amount)
}
}
type Vehicle = Car | Truck
const v1 = new Car()
const v2 = new Truck()
function useVehicle( vehicle: Vehicle ) {
vehicle.drive()
// 透過 可以知道 vehicle 的型別
// 背後雖然 JavaScript 不知道 Truck
// 但可以透過比對 constructor 來知道
if ( vehicle instanceof Truck ) {
vehicle.loadCargo( 1000 )
}
}
useVehicle( v1 )
useVehicle( v2 )
// 藉由在 interface 中加入指定的 literal 型態變數
// 來判斷 interface 的型別
interface Bird {
type: "bird",
flyingSpeed: number
}
interface Horse {
type: "horse",
runningSpeed: number
}
type Animal = Bird | Horse
function moveAnimal( animal: Animal ) {
let speed;
switch( animal.type ) {
case "bird":
speed = animal.flyingSpeed
break
case "horse":
speed = animal.runningSpeed
break
}
consoel.log( 'Moving at speed: ' + speed )
}
moveAnimal({ type: "bird", flyingSpeed: 10 })
HTML
<input type="text" id="user-input">
TypeScript
let userInputElement = document.getElementById("user-input")
// 直接告知 TypeScript 這個變數的型別,來使用相關的函式或變數
if ( userInputElement ) {
( userInputElement as HTMLInputElement ).value = "Hi there!"
}
interface ErrorContainer {
[ prop: string ]: string;
}
// 可以接受 key 值為 string, value 值為 string 的 n 個屬性
const errorBag: ErrorContainer = {
email: "Not a valid email",
username: "Must start with a capital character!"
}
type Combinable = number | string
// 函式多載要求最下方的函式定義的參數型別及回傳型別
// 需要能兼容上面所有的多載方法
function add( a: string, b: string ): string;
function add( a: number, b: string ): string;
function add( a: string, b: number ): string;
function add( a: Combinable, b: Combinable ) {
if ( typeof a === "string" || typeof b === "string" ) {
return a.toString() + b.toString();
}
return a + b;
}
ES6內容 跳過
ES6內容 跳過
/**
* 當我們希望我們定義的資料型態
* 能對廣泛不同類型做相同的處理
* 這邊要如何定義"廣泛不同類型"
* 就需要使用到 Generics ( 泛型 )
* 即為 C++ 的泛型
*/
/* 範例 */
// Array 可以接受多種型別
const names: Array<string> = ["hello", "world"]; // 效果等同 string[]
// Promise 回傳可以有多種型別
const promise: Promise<string> = new Promise(( resolve, reject ) => {
setTimeout(() => {
resolve( "This is done !" )
}, 1000)
});
promise.then( data => {
// 若無法手動定義 resolve 回傳的型別,則無法知道回傳值的任何參數或方法
data.split(" ")
})
function merge<T, U>( objA: T, objB: U ) {
return Object.assign( objA, objB )
}
// TypeScript 會自動抓取參數的型別作為泛型的型別
// typeof T === { "name": string }
// typeof U === { "age": number }
const mergedObject = merge( { name: "Max" }, { age: "30" } )
// 因為已經知道 T 的型別了,所以可以直接呼叫 .name
console.log( mergedObject.name )
function merge<T extends object, U extends object>( objA: T, objB: U ) {
return Object.assign( objA, objB )
}
// 第二個參數報錯
// 因為 U 不接受 object 以外的型別
const mergedObject = merge( { name: "Max" }, 30 )
// 保證有名叫 length 的參數
interface Lengthy {
length: number
}
function countAndDescribe<T extends Legnthy>( element: T ) {
let descriptionText = "Got no value"
// 需要有 element.length
if ( element.length > 0 ) {
descriptionText = `Got ${ element.length } value${ element.length === 1 ? "" : "s" }.`
}
}
// 把從 T 得到的 keys 視為一個子集合,確保 U 屬於該子集合
function extractAndConvert<T extends object, U extends keyof T>(
obj: T,
key: U
) {
return "Value: " + obj[ key ]
}
// example
class DataStorage<T extends string | number | boolean> {
private data: T[] = []
addItem( item: T ) {
this.data.push( item )
}
removeItem( item: T ) {
if ( this.data.indexOf( item ) === -1 ) {
return
}
this.data.splice( this.data.indexOf( item ), 1 )
}
getItems() {
return [ ...this.data ]
}
}
interface CourseGoal {
title: string,
description: string,
completeUtil: Date
}
function createCourseGoal(
title: string,
description: string,
date: Date
): CourseGoal {
let goal: Partial<CourseGoal> = {}
courseGoal.title = title
courseGoal.description = description
courseGoal.completeUtil = date
return goal
}
const names: Readonly<string[]> = ["Max", "Anna"]
names.push("Mahumahu") // 會出現錯誤