Skip to content

Keyof Type Operator

tags: TypeScript

參考資料: TypeScript Handbook冴羽 TypeScript 系列

The keyof type operator

對一個物件型別使用 keyof 型別運算子,會回傳由物件的 key 值組成的字串或數值字面值的聯合 (union),在下面的例子中,P 的型別就等同於 "x" | "y":

ts
type Point = { x: number; y: number };
type P = keyof Point;
/* type P = keyof Point */

如果這個型別有 string 或是 number 的索引簽章,keyof 會直接回傳這些型別:

ts
type Arrayish = { [n: number]: unknown };
type A = keyof Arrayish;
/* type A = number */

type Mapish = { [k: string]: boolean };
type M = keyof Mapish;
/* type M = string | number */

這裡要注意的是,在這個例子裡 M 的型別為 string | number,這是因為在 JavaScript 中物件的 key 值會被強制轉型成 string,所以 obj[0]obj['0'] 是相同的。

(註:原文到這就結束了,以下引用自冴羽大大在他的 TS 系列文章的補充)

數值字面值聯合型別

前面也提到過 keyof 可能回傳一個數值字面值 (numeric literal) 的聯合型別,那究竟什麼時候會回傳它呢,可以試著撰寫這樣一個物件:

ts
const NumericObject = {
  [1]: '梓一號',
  [2]: '梓二號',
  [3]: '梓三號',
};

type result = keyof typeof NumericObject;

/*
NumericObject 的 type 為:
{
  1: string;
  2: string;
  3: string;
}
所以這個 result 的型別最後就是:
type result = 1 | 2 | 3
*/

Symbol

TypeScript 亦支持 symbol 型別的屬性名:

ts
const sym1 = Symbol();
const sym2 = Symbol();
const sym3 = Symbol();

const symbolToNumberMap = {
  [sym1]: 1,
  [sym2]: 2,
  [sym3]: 3,
};

type KS = keyof typeof symbolToNumberMap;
/* type KS = typeof sym1 | typeof sym2 | typeof sym3 */

這也是為什麼在泛型中寫出像下面這個例子的程式碼會報錯:

ts
function useKey<T, K extends keyof T>(obj: T, key: K) {
  let name: string = key;
  /*
  類型 'string | number | symbol' 不可指派給類型 'string'。
    類型 'number' 不可指派給類型 'string'。ts(2322)
  */
}

如果今天很確定只使用字串型別的屬性名,你可以這樣寫:

ts
function useKey<T, K extends Extract<keyof T, string>>(obj: T, key: K) {
  let name: string = key; // OK
}

而如果要處理所有的屬性名,可以這樣寫:

ts
function useKey<T, K extends keyof T>(obj: T, key: K) {
  let name: string | number | symbol = key;
}

類別和介面

對類別使用 keyof

ts
class Person {
  name: 'Sheep';
}

type result = keyof Person;
/* type result = "name" */
ts
class Person {
  [1]: string = 'sheep';
}

type result = keyof Person;
/* type result = 1 */

對介面使用 keyof

ts
interface Person {
  name: string;
}

type result = keyof Person;
/* type result = "name" */

實戰

在前一節泛型裡就寫到了一個 keyof 的應用:

我們想要從一個物件中拿到給定屬性名的值,我們必須確保我們不會意外抓取物件上不存在的屬性,因此我們要在這兩種型別中放置一個限制:

ts
function getProperty<Type, Key extends keyof Type>(obj: Type, key: Key) {
  return obj[key];
}

let x = { a: 1, b: 2, c: 3, d: 4 };

getProperty(x, 'a');