สรุป Syntax ของ TypeScript หากจะย้ายจาก JavaScript ต้องรู้อะไรใหม่บ้าง

บทความนี้จะมาสรุป Syntax เรื่องการเขียน TypeScript สำหรับคนที่เขียน JavaScript เป็นประจำอยู่แล้ว (ซึ่งผมกำลังจะย้ายมาเขียน TS) หากจะย้ายมาเขียน TypeScript หรือทำการ Migrate โปรเจคที่เคยทำไว้ให้เป็น TypeScript เราจะต้องแก้อะไรบ้าง ต้องบอกก่อนว่าผมไม่ได้มาสอนทั้งหมด เพียงแค่เบี้องต้นไว้สำหรับทำงานช่วงแรกๆ ก่อน สำหรับการใช้ฟีเจอร์ที่ลึกลงไป ถ้ามีโอกาสผมจะมาอธิบายให้ฟัง เพราะตอนนี้ผมก็เพิ่งหัดใช้เหมือนกัน แต่ก่อนอื่นเลยไปติดตั้ง TypeScript กันก่อนที่ link
Why TypeScript
เหตุผลที่คุณควรเขียน TypeScript เพราะมัน:
- เป็น Complie Time Error เราจะสามารถเห็น Error ได้ตั้งแต่ตอนเขียนว่าระบบเราจะบัคตรงไหนบ้าง
- รองรับการเขียนแบบ Strong type หรือ Static type หรือพูดง่ายๆ ว่ามีต้องประกาศ type ของตัวแปร
- ทำงานได้กับทุกๆ Browser และ JS Engine
- ใช้เวลาเขียนนานหน่อยแต่ง่ายต่อการ debug และการหา Error มาก
- เหมาะกับการใช้ JavaScript ในการทำระบบที่มีขนาดใหญ่ อย่างเช่น NodeJS เพราะสาเหตุของการเป็น Typesafe
- JavaScript เป็น Runtime Error เพราะงั้นเราจะไม่เจอบัคจนกว่าจะรันไฟล์
เหตุผลคร่าวๆ คงจะประมาณนี้ผมคงจะไม่ลงลึกมาก เดี๋ยวเราจะมาเริ่มสรุป Syntax กันเลย
Variable Declaration
เราสามารถใช้ var
, let
, const
ได้ปกติ แต่เราจะมีการประกาศ Type ของตัวแปรเข้าไปด้วย เรามาดู Data type ที่ใช้กันเป็นประจำกันก่อน
Primitive
// Number
const n1: number = 1
const n2: number = 1.1// Boolean
const n3: boolean = true// Array
const n5: string[] = ['1','2','3']// String
const n8: string = 'It\'s a string'
Generic
รองรับการทำ Generic สำหรับสาย Java, C# น่าจะถูกใจอย่างมาก หากใครอยากศึกษามากกว่านี้แนะนำว่าลองอ่านในบทความนี้ครับ link
const n1: Array<String> = ['1','2','3']
const n2: Array<any> = [1, 'Apple',true, 1.2]
Any
ในกรณีที่เราอยากให้ตัวแปรเรารับ Type อะไรก็ได้ หรือพูดง่ายๆ ว่าทำให้เป็น Dynamic type เราก็จะประกาศเป็น any
ให้มัน ก็จะเหมือนกันการเขียน JavaScript ทั่วไปที่ตัวแปร
let n1: any = 1
n1 = 1.2
n1 = true
n1 = [1,2]
Void
ถ้าใครเขียน Java หรือ C#, C, C++ น่าจะคุ้นเคยกันอย่างดี แต่ถ้าใครไม่เคยเลย มันคือ Type ที่ไว้บอกตัว Complier ว่าไม่มีการ return อะไรออกมาจากฟังก์ชัน เดี๋ยวตรงนี้จะอธิบายในหัวข้อการประกาศฟังก์ชัน หรือถ้าใส่ในตัวแปรก็จะได้ค่า undefined
หรือ null
Tuple
หน้าตาจะคล้ายๆ Array แต่ว่าข้อดีของมันทำให้เราสามารถกำหนด Data type ที่จะเก็บลงใน Array ได้ว่าจะหน้าตาแบบไหนทำให้เราสามารถจัดการของใน Array ให้อยู่ในรูปแบบที่เป็น Fixed type ได้ อย่างเช่น
const arr1: [number, number] = [1, 2]
console.log(arr1) // [1, 2]const arr2: [number, string, boolean] = [1, 'John Doe', true]
Enum
ตัวนี้ช่วยให้เราทำ Constant ได้ง่ายขึ้นมากเพราะปกติถ้าเป็น JS เราจะประกาศเป็นตัวแปรแบบ constant แบบนี้แทน
export const DATE_TIME_FORMAT_24H = 'yyyy-MM-dd HH:mm:ss'
export const VAT = 7 / 100
ที่นี้ลองมาเขียนเป็น enum บ้าง
enum DateTimeFormat {
DATE_TIME_24H = 'yyyy-MM-dd HH:mm:ss',
DATE_TIME_12H = 'yyyy-MM-dd hh:mm:ss',
DATE = 'yyyy-MM-dd'
}enum FileTypeSupported {
jpg = 'jpg',
png = 'png',
mp4 = 'mp4'
}
console.log(FileTypeSupported.jpg) // jpgenum Ans {
No,
Yes
}
console.log(Ans[0]) // No
console.log(Ans.NO) // 0
console.log(Ans[Ans.NO]) // No
Union
ตัวนี้ช่วยให้เราเลือกว่าตัวแปรเราจะมีการใช้ type อะไรได้บ้าง ที่อาจจะมากกว่า 1 การทำงานจะเหมือน Any แต่ Any จะรวมทุกตัว แต่ถ้าเราอยากให้มีแค่ 2 types ล่ะ?
let strOrNum: (number | string) = 1
strOrNum = 'test'strOrNum = false // Errorlet moreThen2: (number | string | boolean) = false // ok
Function Declaration
มาเริ่มที่การเขียนในรูปแบบของ JavaScript ก่อน
function add(n1, n2) {
return n1 + n2
}// ES6 arrow
const add = (n1, n2) => n1 + n2
อย่างที่บอกไปว่า TypeScript เป็นภาษาที่ต้องประกาศ Type ด้วยดังนั้นการประกาศฟังก์ชันก็จะเหมือนกันการสร้างตัวแปรเพราะมันมี Pattern เดียวกัน
function add(n1: number, n2: number) {
return n1 + n2
}// ES6 arrow
const add = (n1: number, n2: number) => n1 + n2
โค้ดทั้งหมดสามารถทำงานได้อย่างไม่เกิด Error ใดๆ แต่เมื่อเราประกาศ Type ให้กับ Input แล้วเราก็ควรประกาศให้กับ Output ด้วยเพื่อให้ Flow ของ Data ในระบบเป็น Type เดียวกันจนจบการทำงาน
function add(n1: number, n2: number): number {
return n1 + n2
}const add = (n1: number, n2: number): number => n1 + n2
จะเห็นว่าผมใส่ return type ไปด้วยให้ออกมาเป็น number
ซึ่งจะทำให้เราประกาศ function signature ได้ในที่นี้หมายถึงการทำให้ฟังก์ชันทำหน้าที่รับ input เป็นตัวเลข และส่งตัวเลขออกมาเท่านั้น เมื่อใดที่ n1 + n2
ได้ผลที่ไม่ใช้ number ตัว Complier จะแจ้งเราทันทีว่าเราทำผิด signature อยู่
เราสามารถใส่ void
เข้าไปได้ถ้าต้องการให้ฟังก์ชันจะไม่มีการ return ค่าใดๆ ออกมา
function add(n1: number, n2: number): void {
return n1 + n2 // Error
}const add = (n1: number, n2: number): void => n1 + n2 // Error--------------------------------------------------------
function add(n1: number, n2: number): void {
console.log(n1 + n2) // ok
}const add = (n1: number, n2: number): void => {
console.log(n1 + n2) // ok
}
ทำไมต้องใส่ return type ทั้งๆ ที่ไม่ใส่ก็ได้? สาเหตุเลยมันช่วยให้การอ่านโค้ดทำได้ง่ายขึ้นเพราะเราจะสามารถคาดการผลลัพธ์ของฟังก์ชันที่จะออกมาได้ว่าเป็นอะไร ก่อนที่เราจะทำการรันโค้ด เราอาจจะเคยเจอว่ามีคนถามว่าฟังก์ชันนี้ return อะไรออกมา JSON?, String?, Int? มันทำให้เรารู้ได้เลยว่า เราไม่ได้เขียนฟังก์ชันที่ส่ง Bug ออกก็คือส่งค่ามาผิด Type นั้นเอง และยังช่วยให้การอ่านโค้ดหาว่าบรรทัดไหนทำให้ Signature ของฟังก์ชันเปลี่ยนไป ก็จะได้แก้ถูกจุด
Optional parameter
เราสามารถทำให้ฟังก์ชันเรารับหรือไม่รับ Args ที่ประกาศไว้ก็ได้ด้วยทำเป็น Optional ไว้ เพียงแค่ใส่ ?
ไว้ที่ตัวแปร
const add = (n1: number, n2: number): number => n1 + n2
add(1) // Errorconst add = (n1: number, n2?: number): number => n1 + n2add(1) // ok got NaN
Default value
การทำ Optional อีกรูปแบบนึงคือการใส่ Default value เพื่อป้องกันตัวแปรเราจะเป็น undefined
และทำให้การทำงานผิดพลาด เพราะดูจากโค้ดด้านบนเราจะเห็นว่า n1 + undefined
ผลที่ได้คือ NaN ซึ่งหมายความระบบจะบัคตรงนี้ เพื่อเลี่ยงอะไรแบบนี้ผมขอแนะนำท่านี้
const add = (n1: number, n2: number = 2): number => n1 + n2add(1) // 3
เพียงแต่ลบ ?
ออก แล้วเราก็ใส่ค่า default เข้าไปแทนเท่านี้ฟังก์ชันเราก็ทำงานได้แล้ว
Interface
หน้าที่ของ Interface นั้นมีประโยชน์มากมันช่วยให้เราประการหน้าตาของ Input หรือ Output ของตัว ฟังก์ชันหรือแม้แต่ Class ควรเป็นแบบไหน หรือพูดอีกอย่างว่าเราสามารถทำ signature ของ Class หรือฟังก์ชันได้ เรามาดูตัวอย่างการใช้งาน Interface กันว่าเราสามารถทำในลักษณะไหนได้บ้าง
Function arg input
interface IPerson {
firstName: string
lastName: string
}function sayHiToPerson(info: IPerson) {
return `Hi ${info.firstName} ${info.lastName}`
}sayHiToPerson({ firstName: "John", lastName: "Doe" }) // Hi John DoesayHiToPerson({ lastName: "Doe" }) // Error missing firstNamesayHiToPerson({ test: 1 }) // Error missing wrong interface
เราสามารถประกาศหน้าตาของ input ได้ว่าต้องการให้เป็นแบบไหนโดยการสร้าง interface ขึ้นมา จากนั้นจึงมาประกาศเป็น type ของ input หากมีการส่งที่ไม่อยู่ใน interface มาเป็น parameter ระบบก็จะแจ้ง Error ทันที
Function output
interface IPerson {
firstName: string
lastName: string
}function createPerson(firstName: string, lastName: string): IPerson {
return { firstName, lastName };
}console.log(createPerson("John", "Doe"));
// { firstName: 'John', lastName: 'Doe'}
เราจะสามารถทำให้ output ของฟังก์ชันเราถูกกำหนดด้วย interface ได้เพื่อการันตีตัว signature ว่าจะออกมาเป็น Object ที่มี firstName
และ lastName
เท่านั้น ถ้าเกิดเราดันไปเขียนให้ส่วนของ return เป็น
return { firstName, lastName, age: 10 };
แบบนี้จะ Error ทันทีเพราะใน Interface ไม่มี age
อยู่
Class interface
interface Person {
fullName: string;
toString();
}class Employee implements Person {
empID: string;
fullName: string;constructor (eID: string, name: string) {
this.empID = eID;
this.fullName = name;
}toString() {
console.log(`EMP ID of ${this.fullName} is ${this.empID}`);
}
}const emp1 = new Employee('E11', 'John Doe')
console.log(emp1);
// Employee { empID: 'E11', fullName: 'John Doe' }console.log(emp1.toString()) // EMP ID of John Doe is E11
เราสามารถทำ Interface ให้ class ได้อีกด้วยถ้าหากใครเขียน OOP เป็นหลัก
Interface optional
interface IPerson {
firstName?: string
lastName: string
}
เราสามารถใช้ ?
เพื่อเป็นการบอกว่าไม่จำต้องรับ parameter ตัวไหนบ้างเหมือนกับการประกาศฟังก์ชันก่อนหน้านี้ก็ได้
Default value
ตัว Interface ไม่สามารถทำ Default value ได้ครับ เพราะมันเป็น Abstract layer ไม่ใช่ส่วนของ Implement layer เพราะงั้นเราต้องไปทำเองใส่ส่วนของ Implement layer ก็คือพวกที่เรียกใช้งาน Interface นั้น และการทำ default value นั้นเราจะต้องทำคู่ไปกับการใช้ destructuring และการทำ optional เดี๋ยวผมจะทำให้ดู
interface IPerson {
firstName?: string
lastName: string
}function sayHi({ firstName = 'Default' , lastName }: IPerson): any {
console.log(`Hi ${firstName} ${lastName}`);
}sayHi({ lastName: 'Doe'}) // Hi Default Doe
ผมทำให้ firstName
เป็น optional ไว้ก่อน ถ้าไม่งั้นมันจะฟ้อง Error ว่า firstName
หายไป จากนั้นผมก็ใช้ทำการ destructuring ตัว Object ใน sayHi
เพื่อเอาค่าที่ต้องการจาก Interface พูดง่ายๆว่า ก็คือผมแกะ firstName
และ lastName
ออกมาแล้วใส่ Default
ให้กับตัว firstName
เพราะมันเป็น Optional ไปแล้วถ้าเกิดลืมใส่มาระบบเราก็จะไม่พัง และส่วนบรรทัดถัดไป
sayHi({ lastName: 'Doe'})
ผมก็โยนค่าเข้าไปแต่ไม่ใส่ firstName
แล้วตัวระบบจะรู้เองว่ามันไม่มีค่ามา จากนั้นมันก็จะเอาค่าที่ใส่เป็น default ไว้มายัดให้แทน ตรงนี้ไม่ใช่การทำงานของ TS แต่มันการเขียนโปรแกรมทั่วไปของ ES6 ที่เราเอามาประยุกต์ให้เข้ากับตัว TS แค่นั้นเองครับ สำหรับคนที่ไม่เข้าใจเรื่อง destructuring แนะนำว่าไปอ่านจาก developer.mozilla ครับ สำหรับประโยชน์การเรียก method โดยการโยน Object เข้าไปแทนการเรียกแบบปกติ ก็คือ sayHi(arg1, arg2, .... argN)
และทำไมเราควรเขียน JS ให้รับ Object แทนนั้นไว้บทความหน้าผมจะมาเล่าให้ฟังนะครับ เดี๋ยวจะเอาลิ้งมาแปะให้ในนี้
หน้าที่ของ interface ยังไม่หมดเท่านี้ ถ้าสนใจลองไปอ่านต่อที่ devblogs.microsoft
ES6 Feature
ตอนเขียนเราก็ยังสามารถใช้ ES6 เขียนได้ปกตินะครับ เพราะตอนที่เราจะเอาระบบที่เขียนด้วย TS ไปทำงาน เราจะต้อง Complie มันก่อนเป็น JS ปกติ มันยังไม่สามารถเอาไปทำงานได้เลย ซึ่งการ complie จะอยู่ในการตั้งค่าไฟล์ tsconfig.json (อ่านหัวข้อถัดไป) ในไฟล์ tsconfig.json เราจะสามารถบอกให้ TS ทำการ complie เป็น JS เวอร์ชันอะไรก็ได้ไม่ว่าจะ ES5 หรือ ES6 และการ complie ก็ง่ายมากๆ แค่พิมพ์ tsc
มันก็จะทำการสร้างโค้ด JS ให้เราทันทีก็เอาตรงนี้ไปทำงานต่อได้
How to migrate
วิธีการเปลี่ยนโปรเจคที่ทำอยู่เป็น TS
- เปลี่ยนชื่อไฟล์
.js
เป็น.ts
- ใส่ไฟล์ tsconfig.json (link)
- ใส่ Type ให้กับตัวแปร
- เช็ค Error ที่เกิดจาก Type ผิด อันไหนไม่แน่ใจ type ใส่ any ไปก่อน
- มีเวลาก็มาทำ Interface ให้เพื่อทำ Function signature
คร่าวๆ คงมีประมาณนี้ครับ
สำหรับมือใหม่ในการหัดเขียน TS ผมก็คิดว่าเท่านี้ก็พอทำงานได้แล้วครับ เพราะคำสั่งที่เหลือจะเหมือนกับ JS ทุกอย่างเลย สำหรับบทความนี้ผมจะไม่ลงลึกไปมากในตัวของ Feature ของ TS เพราะ ถ้าใครจะอ่านต่อ ผมแนะนำว่าลองศึกษาที่เว็บหลักของ TS ที่ www.typescriptlang.org ได้เลยครับ