[TS] TypeScript Basic Type
此篇為各筆記之整理,非原創內容,資料來源可見下方連結與文後參考資料。
- Basic Types @ TypeScript
- Top Types:指的是可以接受所有值的型別,包含
any
和unknown
。 - Bottom Types:指的是不能接受任何值的型別,即
never
。
Array and Tuple
- The Array Type @ TypeScript > Object Types
- The ReadonlyArray Type @ TypeScript > Object Types
- Tuple Types @ TypeScript > Object Types
- readonly Tuple Types @ TypeScript > Object Types
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>
的。
Any
any
和 never
相反,可以把 any
視為什麼都包含在內的全集合(Universe),它包含了所有可能性。
一開始宣告變數時,如果對變數不設值,且為註記型別,都會被視為是 any:
let foo; // any
type A = 'string' | 'number' | any; // type A = any
never
- never 可以想成是一個什麼都沒有的「空集合(empty set)」
// 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
unknown
和 any
一樣,該型別的變數都可以帶入任意的值(屬於 Top Types),但有一個關鍵的差異在於,開發者不能對 unknown
型別的變數進行任何操作,但 any
可以:
let isAny: any;
let isUnknown: unknown;
isAny.hello(); // 可以對 any 型別進行任何操作
isUnknown.hello(); // 不可以對 unknown 型別進行任何操作
Unknown 適合用在
- 當該變數不希望再被操作的情況
- 該變數型別未知,需要在被限縮(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();
Nullable Types
包含 null
和 undefined
:
// Not much else we can assign to these variables!
let u: undefined = undefined;
let n: null = null;
Object
任何除了原始型別(primitive type)之外的,都可以算是 Object Type。原始型別包含:數值、字串、布林、symbol
、null
、undefined
。
使用 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 才能確保型別的正確性:
這時候,透過 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 時的行為。
參考
- 讓 TypeScript 成為你全端開發的 ACE! @ 第 11 屆 iT 邦幫忙鐵人賽 by Maxwell
- React TypeScript: Basics and Best Practices
- TypeScript Tutorial for JS Programmers Who Know How to Build a Todo App
- Basic Types @ TypeScript handbook