[Day01] Typescript與Javascript差異

因為未來工作長期會使用到Typescript,所以就趁這時候筆記一下與JS的差異方面。
過去在寫Vue3、Angular時有大量接觸Typescript,就趁這時間重溫一下。

  1. 賦值

JS 是動態型別 → 錯誤在執行時才發現
TS 是靜態型別 → 編譯時就檢查出錯,減少 bug

JS

1
2
let age = 25;
age = "twenty-five"; // ✅ JS 不會報錯,可能導致執行期錯誤

TS

1
2
3
let age: number = 25;
age = "twenty-five";
// ❌ 編譯錯誤:Type 'string' is not assignable to type 'number'
  1. 函數參數與回傳型別
    TS 允許定義 參數型別 和 回傳型別,防止不合法輸入

TS

1
2
3
4
5
function add(a: number, b: number): number {
return a + b;
}
add(1, "2");
// ❌ Argument of type 'string' is not assignable to parameter of type 'number'

3.可選參數及預設值

TS 用 ? 宣告參數可選
搭配 nullish coalescing (??) 處理 null 或 undefined

1
2
3
4
function greet(name?: string) {
console.log(`Hello, ${name ?? "Guest"}`);
}
greet(); // Hello, Guest

JS則是用||處理

JS

1
2
3
4
function greet(name) {
console.log(`Hello, ${name || "Guest"}`);
}
greet(); // Hello, Guest

4.Interface

TS interface 能保證物件結構正確,JS 則只能在執行時才發現問題

TS

1
2
3
4
5
6
7
8
9
10
11
interface User {
name: string;
age: number;
}

function printUser(user: User) {
console.log(user.name, user.age);
}
printUser({ name: "Alex", age: 25 }); // ✅
printUser({ name: "Alex" });
// ❌ Property 'age' is missing in type ...

5.Enum vs. Magic String

Enum 可集中管理常量並提供型別檢查,避免拼錯問題
TS

1
2
3
4
5
6
7
8
9
10
enum Status {
Success = "success",
Fail = "fail"
}

const status: Status = Status.Success;

if (status === Status.Success) {
console.log("OK");
}

JS則無
JS

1
2
3
4
const status = "success"; // Magic string,容易拼錯
if (status === "succes") { // 拼錯不會報錯
console.log("OK");
}

6.參數多種型別檢查Union Types

TS:

1
2
3
4
5
6
7
function printId(id: number | string) {
console.log(id);
}
printId(123);
printId("abc");
printId(true);
// ❌ Argument of type 'boolean' is not assignable to parameter of type 'string | number'
  1. 泛型 (Generics)

any 會失去型別資訊
泛型保留型別,讓函數既安全又通用
TS:

1
2
3
4
5
6
function identity<T>(arg: T): T {
return arg;
}

const a = identity(123); // a: number
const b = identity("hi"); // b: string

一般JS不好確認型別
JS:

1
2
3
function identity(arg) {
return arg; // 無法知道回傳型別
}

8.非空斷言與 Optional Chaining

?. 可安全存取深層屬性
搭配 ! 非空斷言在確定不為空時跳過檢查
TS:

1
2
const user: any = {};
console.log(user?.profile?.name); // ✅ undefined

JS則無法
JS:

1
2
const user = {};
console.log(user.profile.name); // ❌ TypeError
  1. any vs. unknown
    什麼時候用 any,什麼時候用 unknown?

JS:

1
2
let value; // 無型別限制
value.foo(); // 可能出錯

any 跟 JS 一樣完全不檢查型別(危險)
unknown 要先檢查型別才可使用(安全)

TS:

1
2
3
4
5
6
7
let value: unknown;
value = "hello";
// value.toUpperCase(); ❌ 必須先做型別檢查

if (typeof value === "string") {
console.log(value.toUpperCase()); // ✅
}
  1. 類別 (Class) 的型別修飾符

JS 與 TS 在類別屬性可見性上的差異
JS:

1
2
3
4
5
class Person {
constructor(name) {
this.name = name; // 無法限制外部訪問
}
}

TS 提供 public(預設)、private、protected,可精確控制存取範圍

TS:

1
2
3
4
5
6
class Person {
private name: string; // 僅類別內可訪問
constructor(name: string) {
this.name = name;
}
}
  1. 型別別名(Type Alias)與複雜型別組合

大型專案中可以將複雜型別命名,方便重用與維護
TS:

1
2
3
4
type OrderStatus = "pending" | "shipped" | "delivered";
type Order = { id: number; status: OrderStatus };

function process(order: Order) { /* ... */ }
  1. Intersection Types(交叉型別)合併資料結構

JS:

1
2
// 只能動態合併物件
const person = Object.assign({}, { name: "Alex" }, { age: 25 });

交叉型別在多個模組資料合併時很常用(特別是 Redux / API 結果整合)

TS:

1
2
3
4
5
type Name = { name: string };
type Age = { age: number };
type Person = Name & Age; // 交叉型別

const person: Person = { name: "Alex", age: 25 };
  1. 型別守衛(Type Guards)

JS

1
2
3
function handle(val) {
if (val.start) val.start(); // 可能 undefined
}

大型專案裡經常用自訂型別守衛來精準縮小型別範圍

TS

1
2
3
4
5
6
7
8
9
function isFunction(value: unknown): value is Function {
return typeof value === "function";
}

function handle(val: unknown) {
if (isFunction(val)) {
val(); // ✅ 已縮小型別
}
}
  1. 宣告檔(.d.ts)與第三方函式庫型別

JS:

1
2
3
// 使用第三方庫時,無法獲得型別提示
import moment from "moment";
moment().format("YYYY-MM-DD"); // 沒 IntelliSense

大型專案幾乎都要用 .d.ts 來補齊沒有型別定義的第三方套件
TS:

1
2
3
// @types/moment 提供型別定義
import moment from "moment";
moment().format("YYYY-MM-DD"); // ✅ 型別提示 + 自動完成

15.readonly 與 Immutable 資料結構

readonly 對大型專案的設定檔、常量物件非常重要

Typescript

1
2
3
4
5
6
7
type Config = {
readonly port: number;
};

const config: Config = { port: 3000 };
config.port = 4000;
// ❌ Cannot assign to 'port' because it is a read-only property
  1. keyof 與型別安全的物件鍵名存取

JS

1
2
3
function getProp(obj, key) {
return obj[key]; // key 可能錯拼
}

keyof 確保傳入的屬性名稱是物件中真實存在的鍵名

TS

1
2
3
4
5
6
7
8
function getProp<T, K extends keyof T>(obj: T, key: K) {
return obj[key];
}

const user = { id: 1, name: "Alex" };
getProp(user, "name"); // ✅
getProp(user, "age");
// ❌ Argument of type '"age"' is not assignable to parameter of type '"id" | "name"'
  1. Mapped Types(映射型別)

JS:

1
2
// 只能手動定義每個屬性為可選
const partialUser = { name: "Alex" };

大型專案中 Partial、Required、Readonly 等都是 Mapped Types 的應用

TS:

1
2
3
4
type User = { id: number; name: string; age: number };
type PartialUser = { [K in keyof User]?: User[K] };

const partialUser: PartialUser = { name: "Alex" };
  1. 模組與命名空間(Modules vs Namespaces)

JS:

1
2
// 無型別檢查的全域污染
window.myApp = {};

大型專案建議使用 ES Modules 搭配 TS 的型別系統管理命名與依賴

TS:

1
2
// 使用模組系統
export const myApp = {};
  1. 型別推斷與 as const
1
2
const roles = ["admin", "user"];

as const 在定義固定字面值陣列、物件時非常有用(例如權限、狀態碼)

1
2
3
const roles = ["admin", "user"] as const;
// roles: readonly ["admin", "user"]
type Role = typeof roles[number]; // "admin" | "user"
  1. Non-null Assertion(非空斷言)與嚴格模式

JS:

1
2
let el = document.getElementById("myDiv");
el.style.color = "red"; // el 可能是 null

嚴格模式 (strictNullChecks) 在大型專案中很重要,可以提早發現 null/undefined 錯誤

TS:

1
2
let el = document.getElementById("myDiv")!;
el.style.color = "red"; // ✅ 告訴 TS 這裡一定不為 null

C# Flatten data by Linq

When I work on the project, I usually run into a problem about flatting data.
Because the original data could in the nested property, I have to get it out.
The following is an example about nested properties, and the requirement is that I must get all OrderDetailNumber.

Finally, I found an easy method that is use SelectMany.

LINQ SelectMany

Projects each element of a sequence to an IEnumerable and flattens the resulting sequences into one sequence.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
using System;
using System.Linq;
using System.Collections.Generic;

namespace FlattenResult
{
internal class Program
{
public class Order
{
public string OrderNumber { get; set; }

public List<OrderDetail> orderDetails { get; set; }

public class OrderDetail
{
public string OrderDetailNumber { get; set; }
}
}

static void Main(string[] args)
{
var orders = new List<Order>
{
new Order
{
OrderNumber = "202305121111",
orderDetails = new List<Order.OrderDetail>
{
new Order.OrderDetail
{
OrderDetailNumber = "OD0001",
},
new Order.OrderDetail
{
OrderDetailNumber = "OD0001",
}
}
},
new Order
{
OrderNumber = "202305121112",
orderDetails = new List<Order.OrderDetail>
{
new Order.OrderDetail
{
OrderDetailNumber = "OD0003",
},
new Order.OrderDetail
{
OrderDetailNumber = "OD0004",
}
}
}
};

var orderDetailNumbers = orders.SelectMany(s => s.orderDetails)
.Select(s => s.OrderDetailNumber)
.ToList();

orderDetailNumbers.ForEach(f =>
{
Console.WriteLine(f);
});
}
}
}

Output:

1
2
3
4
OD0001
OD0001
OD0003
OD0004

C# Use AutoMapper to map data model

When writing data to SQL Server, there is always a process about mapping model.
The process is a transformation, such as A model transforming B model.
If the model contains too many properties, the code could became very long.
So we can use a plugin, AutoMapper, to avoid that.

AutoMapper : https://docs.automapper.org/

The following is an example of implementation about converting PersonModel to PersonDto, which is a list convert.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
using System;
using System.Collections.Generic;

using AutoMapper;

namespace AutoMapExample
{
public class Program
{
public class PersonModel
{
public string PersionalID { get; set; }
public string Name { get; set; }
public int? Age { get; set; }
}
public class PersonDto
{
public int? Id { get; set; }
public string Pid { get; set; }
public string Name { get; set; }
public int? Age { get; set; }
}
public static void Main()
{
var personlist = new List<PersonModel>
{
new PersonModel
{
PersionalID = "E111111111",
Name = "TOM",
Age = 20,
},
new PersonModel
{
PersionalID = "B111111111",
Name = "J.Cole",
Age = 26,
}
};

var config = new MapperConfiguration(cfg =>
{
cfg.CreateMap<PersonModel, PersonDto>()
.ForMember(dest => dest.Pid, opt => opt.MapFrom(src => src.PersionalID))
.ForMember(dest => dest.Id, opt => opt.Ignore());
});

// Check for missing mapped properties.
config.AssertConfigurationIsValid();

var mapper = config.CreateMapper();
var dtoList = mapper.Map<List<PersonModel>, List<PersonDto>>(personlist);

dtoList.ForEach(x =>
{
Console.WriteLine($"id:{x.Id},pid:{x.Pid},name:{x.Name},age:{x.Age}");
});
}
}
}

output

1
2
id:,pid:E111111111,name:TOM,age:20
id:,pid:B111111111,name:J.Cole,age:26

Because id was set to ignore, it’s null when the console prints them.

The following is the mapping of the different name properity. Due to the name of the properity is different so I have to define a rule to map, such as PersionalID => Pid.

1
ForMember(dest => dest.Pid, opt => opt.MapFrom(src => src.PersionalID))

The following is a check for missing mapped properties.

1
config.AssertConfigurationIsValid()

Use C# SemaphoreSlim to lock async functions

When writing an async function or method, I encountered a problem with accessing data one-by-one, such as starting 100000 async APIs, which could lead to bugs.

Below is a problem example. I expect the result to be “Sum:100000”, but the actual result is “Sum:99995” or other abnormal results.

Example 1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
namespace SemaphoreSlimExample;

public class Program
{
private static int _sum;

public static void Main(string[] args)
{
Task.WaitAll(Enumerable
.Range(0, 100000)
.Select(x => Add(1))
.ToArray());
Console.WriteLine($"Sum:{_sum}");
}

private static async Task Add(int num)
{
await Task.Run(() =>
{
_sum += num;
});
}
}

Result:

1
Sum:99995

To fix this problem, I added the SemaphoreSlim to code, and the result always is correct “Sum:100000”.
It helps us lock and limit the asynchronous function’s data to one-by-one.

What is Semaphoreslim:
https://learn.microsoft.com/en-us/dotnet/api/system.threading.semaphoreslim?view=net-7.0

Represents a lightweight alternative to Semaphore that limits the number of threads that can access a resource or pool of resources concurrently.

Example2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
namespace SemaphoreSlimExample;

public class Program
{
private static int _sum;
private static readonly SemaphoreSlim locker = new SemaphoreSlim(1, 1);

public static void Main(string[] args)
{
Task.WaitAll(Enumerable
.Range(0, 100000)
.Select(x => Add(1))
.ToArray());
Console.WriteLine($"Sum:{_sum}");
}

private static async Task Add(int num)
{
await locker.WaitAsync();

await Task.Run(() =>
{
_sum += num;
});

locker.Release();
}
}

Result:

1
Sum:100000

C# Distinct objects

In the case, I want to remove the duplicate objects of the list.
I use the object key, UserId and Type, to distinguish them in the list.
After I distinct the objects, the count of the list becomes 2.
The numbers of the list are 2023021403 and 2023021401.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
using System;
using System.Collections.Generic;
using System.Linq;

namespace App
{
internal class Program
{
private class OrderModel
{
public int UserId { get; set; }
public int Type { get; set; }
public string Number { get; set; }
}

static void Main(string[] args)
{
var orders = new List<OrderModel>
{
new OrderModel
{
UserId = 1,
Type = 1,
Number = "2023021401"
},
new OrderModel
{
UserId = 1,
Type = 1,
Number = "2023021402"
}
,
new OrderModel
{
UserId = 1,
Type = 2,
Number = "2023021403"
}
};

var distinctList = orders
.GroupBy(x => new { x.Type, x.UserId })
.Select(x => x.FirstOrDefault())
.ToList();

foreach (var o in distinctList)
{
Console.WriteLine($"Number:{o.Number}");
Console.WriteLine($"UserId:{o.UserId}");
Console.WriteLine($"Type:{o.Type}");
Console.WriteLine("===");
}
}
}
}

Output:

1
2
3
4
5
6
7
8
Number:2023021401
UserId:1
Type:1
===
Number:2023021403
UserId:1
Type:2
===

Simplify queries in SQL Server using CTE

If you use queries in SQL, you could involve complex statements that could cause some maintenance or reading problems. You can still do them, but you can choose a new way, CTE.

What is CTE?
The common table expression (CTE) is a powerful construct in SQL that helps simplify a query.

With CTE, you can split the query into different temporary tables and use them.

Example

If I want to create a report about the total amount of the user’s order, I could use CTE to simplify it.

[dbo].[Orders]

1
2
3
4
5
6
OrderNumber	UserId	Amount
2023020300000001 1 500
2023020300000002 3 100
2023020300000003 1 300
2023020300000004 2 400
2023020300000005 1 100

[dbo].[Users]

1
2
3
4
Id	Name	Age
1 CK 30
2 RR 29
3 NC 25

Then I use the CTE to create a temp table and join them.

Result:

1
2
3
4
Id	Name	Age	OrderTotalAmount
1 CK 30 900
2 RR 29 400
3 NC 25 100

SQL:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
USE OrderDB
GO
WITH Order_User_Total_CTE (UserId, OrderTotalAmount)
AS
-- Define the User Order Total CTE query.
(
SELECT UserId, SUM(Amount) AS OrderTotalAmount
FROM Orders
GROUP BY UserId
)
-- Query
SELECT
U.*,
OUTC.OrderTotalAmount
FROM Users U
LEFT JOIN Order_User_Total_CTE OUTC ON U.Id = OUTC.UserId
GO

Now, if I need to create complex reports, summaries or stored procedures , I will use CTE to help me.

Improve SQL speed by adding index in SQL Server

In the case, I have a simple sql about select, which sorts by date, OrderDate. At the same time, there are 5,000,000 records in the table, Orders. Due to the large amount of data, the SQL speed becomes very slow, about 6 seconds.

1
2
3
4
5
6
7
8
SELECT TOP 1000 [Id]
,[CreatedOn]
,[UpdatedOn]
,[IsValid]
,[OrderDate]
,[OrderNumber]
FROM [OrderDB].[dbo].[Orders]
ORDER BY [OrderDate] DESC

After running the sql script, the speed of the result was very slow, about 6 seconds.

So to imporve this problem, I add the index, NONCLUSTERED INDEX.

1
2
3
4
5
USE OrderDB;  
GO
CREATE NONCLUSTERED INDEX [IX_Orders_OrderDate]
ON Orders ([OrderDate]);
GO

After running the script, I found the speed has been improved by index, about 0 seconds, and there’re still 5,000,000 records in the table.

Generate the test data in SQL Server by stored procedure example

In the case, I want to create a simple script about creating the test data of 100000 records in SQL Server stored procedure.

Today, I have a table, Orders, and I want to generate the test data to it.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
USE [OrderDB]
GO
/****** Object: Table [dbo].[Orders] Script Date: 2/2/2023 3:14:07 PM ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[Orders](
[Id] [int] IDENTITY(1,1) NOT NULL,
[CreatedOn] [datetime] NULL,
[UpdatedOn] [datetime] NULL,
[IsValid] [bit] NULL,
[OrderDate] [datetime] NULL,
[OrderNumber] [varchar](50) NULL,
CONSTRAINT [PK_Orders] PRIMARY KEY CLUSTERED
(
[Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY]
GO

Then I write a simple stored procedure.
You can control the data count you want to generate.

If I want to create the 100000 records, I can do this.

1
2
3
use OrderDB
go
execute dbo.CreateTestData 100000

Below is the complete script, I wrote.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
USE [OrderDB]
GO
/****** Object: StoredProcedure [dbo].[CreateTestData] Script Date: 2/2/2023 2:46:33 PM ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
ALTER PROCEDURE [dbo].[CreateTestData] @Number int
AS
BEGIN
DECLARE
@Counter int = 1
WHILE @Counter <= @Number
BEGIN
INSERT INTO Orders (OrderDate,OrderNumber,IsValid,CreatedOn,UpdatedOn)
VALUES
(
DATEADD(DAY, ABS(CHECKSUM(NEWID()) % 3650), '2000-01-01'),
CONVERT(CHAR(8), FORMAT(GetDate(),'yyyyMMdd')) + right('00000000'+cast(@Counter as varchar(8)),8),
1,
GETDATE(),
GETDATE()
)
SET @Counter= @Counter + 1
END
END

The stored procedure inserts a random date into OrderDate and a sequence number into OrderNumber.

1
2
3
4
5
6
7
Id	CreatedOn	UpdatedOn	IsValid	OrderDate	OrderNumber
1 2023-02-02 15:27:27.763 2023-02-02 15:27:27.763 1 2002-11-30 00:00:00.000 2023020200000001
2 2023-02-02 15:27:27.763 2023-02-02 15:27:27.763 1 2007-04-29 00:00:00.000 2023020200000002
3 2023-02-02 15:27:27.763 2023-02-02 15:27:27.763 1 2002-03-18 00:00:00.000 2023020200000003
4 2023-02-02 15:27:27.763 2023-02-02 15:27:27.763 1 2003-03-27 00:00:00.000 2023020200000004
5 2023-02-02 15:27:27.763 2023-02-02 15:27:27.763 1 2007-11-07 00:00:00.000 2023020200000005
...

簡易後端處理商業邏輯的方式-系統架構篇

在常見的API設計中,透過接收參數到後端服務中,再透過一連串資料操作的邏輯再寫入資料庫或做額外的操作。

這時情況會像是下面這樣:

因為有過多的操作,且這些操作可能在其他的API也會遇到
此時,就可以把他們抽出來成:處理程序(Handler)

透過Handler,把相同的服務可以用的Method或Function整理在一起。

舉個例子:
有支API叫做建立訂單(CreateOrder)
他可能的商業邏輯操作如:建立訂單、修正庫存…等等。
那我們就可以把它抽出來叫做OrderHandler,把複雜的商業邏輯封裝在裡面。

這時如果要使用到建立訂單時就能透過這Handler去幫我們處理。

1
OrderHandler.CreateOrder()

比如API叫做:

1
/Order/CreateOrder

那他的Controller裡面就可以這樣寫

OrderController裡的Action(CreateOrder)就能呼叫OrderHandler.CreateOrder()去協助建立訂單的操作。

這樣做的好處是能集中管理服務的功能,比較不會遇到全都散在Controller的情況。此外,其他Controller要使用到相同的功能,只要呼叫該Handler,而不用整段複製過去。

C#中使用雙問號??取值用法

在C#中可以運用??來判斷前者是否為null,若為null則將後面值遞補上去。
如下,因Amount為空,所以會套用後面的default。
而default就是decimal的預設值0。
因此,在最後會得到amount結果為0。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
namespace Application
{
public class OrderDetail
{
/// <summary>
/// amount
/// </summary>
public decimal? Amount;
}

internal class Program
{
static void Main(string[] args)
{
var orderDetail = new OrderDetail();
var amount = orderDetail.Amount ?? default;
}
}
}