跳至主要内容

[TS] TypeScript Basic Type

此篇為各筆記之整理,非原創內容,資料來源可見下方連結與文後參考資料。

  • Top Types:指的是包含所有可能的型別,全集合,即 unknown
  • Bottom Types:指的是不能接受任何值的型別,空集合,即 never

Array and Tuple

Array and Readonly Array

let list: number[] = [1, 2, 3];
let list: Array<number> = [1, 2, 3]; // 等同於上面的寫法

let list: readonly number[] = [1, 2, 3]; // 不能修改 Array 裡的元素
let list: ReadonlyArray<number> = [1, 2, 3]; // 等同於上面的寫法

Tuple

Tuple 則是有固定長度和元素型別的陣列:

let tuple: [number, string, boolean, number] = [3, 'foo', true, 10];
tuple = [4, 'bar', false, 30];

實際上 Tuple 更像是以 number 作為 key 的物件型別,並且多了 length 屬性,例如:

interface StringNumberPair {
length: 4;
0: number;
1: string;
2: boolean;
3: number;
// other array methods...
}

Tuples with Label(帶有標籤的 tuple)

滑鼠移到該 type 上時,會顯示每個元素的 label

type PersonInfo = [name: string, country: string, age: number];
const aaron: PersonInfo = ['Aaron', 'Taiwan', 32];

Readonly Tuple

如果要把陣列直接當成 readonly tuple 使用,可以在定義陣列時加上 as const,如此它就是會是一個值固定的 readonly tuple,如果沒加上 as const 的話,則會是 string[]

const phoneBrand = ['apple', 'samsung', 'xiaomi', 'sony'] as const;

// readonly tuple
// const phoneBrand: readonly ["apple", "samsung", "xiaomi", "sony"]

Enum

enum Color {
Red,
Green,
Blue,
}
const c: Color = Color.Red; // 0

enum SelectableButtonTypes {
Important = 'important',
Optional = 'optional',
Irrelevant = 'irrelevant',
}

有時物件搭配 enum 是,也會需要 as const 來讓 TS 更明確知道該物件值的型別:

// 範例來自 OneDegree 同事 Ken 與 Ivy

type Nullable<T> = T | null;

enum BE_ENUM_TIME_UNIT {
year = 'year',
month = 'month',
day = 'day',
}

const apiResponse = Math.random() > 0.05 ? BE_ENUM_TIME_UNIT.month : null;

enum FE_ENUM_TIME_UNIT {
year = 'year',
month = 'month',
day = 'day',
}

const map = {
[BE_ENUM_TIME_UNIT.year]: null,
[BE_ENUM_TIME_UNIT.month]: FE_ENUM_TIME_UNIT.month,
[BE_ENUM_TIME_UNIT.day]: FE_ENUM_TIME_UNIT.day,
};

const map2 = {
[BE_ENUM_TIME_UNIT.year]: null,
[BE_ENUM_TIME_UNIT.month]: FE_ENUM_TIME_UNIT.month,
[BE_ENUM_TIME_UNIT.day]: FE_ENUM_TIME_UNIT.day,
} as const;

// 因為沒有用 as const,所以 map[apiResponse] 推倒出來的型別是
// ERROR: Type 'FE_ENUM_TIME_UNIT.year' is not assignable to type 'Nullable<FE_ENUM_TIME_UNIT.month | FE_ENUM_TIME_UNIT.day>'
let unit: Nullable<FE_ENUM_TIME_UNIT.month | FE_ENUM_TIME_UNIT.day> = apiResponse
? map[apiResponse]
: null;

let unit2: Nullable<FE_ENUM_TIME_UNIT.month | FE_ENUM_TIME_UNIT.day> = apiResponse
? map2[apiResponse]
: null;

第一個例子(unit),因為沒有在 map 的型別定義時 as const,所以 map[apiResponse] 會被推導成 FE_ENUM_TIME_UNIT

const map: {
year: null;
month: FE_ENUM_TIME_UNIT;
day: FE_ENUM_TIME_UNIT;
};
const apiResponse: BE_ENUM_TIME_UNIT.month;

map[apiResponse]; // FE_ENUM_TIME_UNIT

導致這個型別(FE_ENUM_TIME_UNIT)沒辦法同時滿足和 Nullable<FE_ENUM_TIME_UNIT.month | FE_ENUM_TIME_UNIT.day>

在第二個例子(unit2)中,因為有在 map2 定義型別時加上 as const,所以 map[apiResponse] 會被推導成 FE_ENUM_TIME_UNIT.month

const map2: {
readonly year: null;
readonly month: FE_ENUM_TIME_UNIT.month;
readonly day: FE_ENUM_TIME_UNIT.day;
};
const apiResponse: BE_ENUM_TIME_UNIT.month;

map[apiResponse]; // FE_ENUM_TIME_UNIT.month

FE_ENUM_TIME_UNIT.month 這個型別是可以滿足 Nullable<FE_ENUM_TIME_UNIT.month | FE_ENUM_TIME_UNIT.day> 的。

never

  • never 可以想成是一個什麼都沒有的「空集合(empty set)」,它是 bottom type
// never 就是一個空
type A = 'string' | 'number' | never; // type A = "string" | "number"

// never 也屬於 any
type T = never extends any ? true : false; // type T = true
  • never 使用的時間點是該函式會「卡在裡出不來」例如,無窮迴圈;或者「終止在函式裡」,例如,拋出錯誤。
  • never 是任何型別的 prototype,因此不用再額外使用 number | never 這種寫法。
  • never 是用來提醒開發者,這個函式執行後,程式將無法繼續往後執行。
function showNever(arg: string | number) {
if (typeof arg === 'string') {
arg.split(',');
} else if (typeof arg === 'number') {
arg.toFixed(2);
} else {
// here, arg is never
}
}

Unknown

unknownnever 相反,可以把 unknown 視為什麼都包含在內的全集合(Universe),它包含了所有可能性,屬於 Top Types。

unknown 的變數有可能是任何型別,但和 any 有一個關鍵的差異在於,開發者不能對 unknown 型別的變數進行任何操作,但 any 可以:

let isAny: any;
let isUnknown: unknown;

isAny.hello(); // 可以對 any 型別進行任何操作
isUnknown.hello(); // 不可以對 unknown 型別進行任何操作

Unknown 適合用在

  1. 當該變數不希望再被操作的情況
  2. 該變數型別未知,需要在被限縮(narrow)後才能使用

因此,對於 unknown 型別,通常會搭配 narrow 或 type guard 使用。

真的必要的話才使用 Type Assertions 的方式:

let isUnknown: unknown = {
foo: 'foo',
hello: () => console.log('hello'),
};

// ❌ 錯誤:不可以對 unknown 型別進行任何操作
isUnknown.foo;
isUnknown.hello();

type Hello = {
foo: string;
hello: () => void;
};

// ⭕️ 正確:對 unknown 型別需要使用 Type Assertions
(<Hello>isUnknown).foo;
(isUnknown as Hello).hello();

any

一開始宣告變數時,如果對變數不設值,且為註記型別,都會被視為是 any:

any 是個特例

any 算是 TypeScript 中的一個「特例」,它有點難用集合的概念來解釋,因為它既是 Top Type 又是 Bottom Type 的型別。比較好的理解是,any 就是在告訴 TypeScript,你就放生我、不要管了吧!

let foo; // any

type A = 'string' | 'number' | any; // type A = any

Nullable Types

包含 nullundefined

// Not much else we can assign to these variables!
let u: undefined = undefined;
let n: null = null;

Object

任何除了原始型別(primitive type)之外的,都可以算是 Object Type。原始型別包含:數值、字串、布林、symbolnullundefined

使用 Object Literal 建立物件

當我們使用 Object literal 建立物件而沒有宣告型別時,實際上 TS 是建立一個 Type,而不是套用 object 型別:

let foo = {
s: 'string',
n: 3,
b: true,
};

實際上這個 foo 套用了一個 Type:

type Foo = {
s: string;
n: number;
b: boolean;
};

因此將不能對 foo 的這物件去新增屬性,例如,foo.bar = 'bar' 是不被允許的。

物件的解構賦值

const aaronChen = { firstName: 'Aaron', age: 31 };
const { firstName, age }: { firstName: string; age: number } = aaronChen;

階層更深的解構賦值:

const aaronChen = {
location: {
zipCode: 110,
city: 'Taipei',
},
};

const {
location: { zipCode, city },
}: { location: { zipCode: number; city: string } } = aaronChen;

在函式參數使用物件的解構賦值

// Use arrow function
const getName = (person: { firstName: string; lastName: string }) => {
const { firstName, lastName } = person;
return `${firstName} ${lastName}`;
};

// Or normal named function
function getName(person: { firstName: string; lastName: string }) {
const { firstName, lastName } = person;
return `${firstName} ${lastName}`;
}

const name = getName({
firstName: 'Aaron',
lastName: 'Chen',
});

Satisfies Operator

在 TypeScript 4.9 後推出新的 satisfies operator,目前看起來適合用在「想要限制物件 key 的型別,但又希望 value 的型別是可以跟著 key 的」情況。

官網提供的例子來說:

// https://devblogs.microsoft.com/typescript/announcing-typescript-4-9/
type Colors = 'red' | 'green' | 'blue';
type RGB = [red: number, green: number, blue: number];

/**
* 希望限制物件 key 的型別只能是 Colors
*/
const palette: Record<Colors, string | RGB> = {
red: [255, 0, 0],
green: '#00ff00',
bleu: [0, 0, 255],
// ~~~~ 能夠偵測到型別的錯誤
};

這樣雖然能夠成功限制物件屬性 key 的型別,但這時候物件 value 的型別會變成 string | RGB,這並不是我們想要,因為這會使得當我們想要使用物件屬性對應的方法時,需要額外加上 type narrowing 才能確保型別的正確性:

image-20221120113115447

這時候,透過 satisfies,如果我們希望能夠限制物件屬性 key 的型別,同時保留該物件屬性 key 所對應到 value 的型別,則可以使用 satisfies

const palette = {
red: [255, 0, 0],
green: '#00ff00',
bleu: [0, 0, 255],
// ~~~~ 能夠偵測到型別的錯誤
} satisfies Record<Colors, string | RGB>;

這時候,當物件 key 的型別不合 Colors 時,TypeScript 同樣會噴錯,同時,TypeScript 會知道 red 的型別時 [number, number, number]green 的型別是 string,而不再是 RGB | string,也就不再需要使用 type narrowing 才能使用該屬性。

Type Assertions

有些時候你會比 TypeScript 對於資料的型別更清楚,這時候透過 Type Assertions,你可以告訴 TypeScript 的編譯器說「請相信我,以我的為準,我知道我自己在做什麼」。

Type Assertions 有兩種主要的用法,一種是使用角括弧(<>),一種是用 as

const data = JSON.stringify({
foo: 'foo',
bar: 'bar',
});

// 使用 <> 進行 type assertions
let parsedJSON1 = <{ foo: string; bar: string }>JSON.parse(data);

// 使用 as 進行 type assertions
let parsedJSON2 = JSON.parse(data) as { foo: string; bar: string };

也可以搭配 Type Alias 使用:

type Data = {
foo: string;
bar: string;
};

// 使用 <> 進行 type assertions
let parsedJSON1 = <Data>JSON.parse(data);

// 使用 as 進行 type assertions
let parsedJSON2 = JSON.parse(data) as Data;

和 type alias 一樣,type assertions 會在 compile 階段被移除,並且不會影響到 runtime 時的行為。

參考