본문으로 건너뛰기

Object.keys의 타입추론

· 약 6분
Mitchell

문제의 상황

const fruits = {
apple: 'apple',
banana: 'banana',
orange: 'orange'
}

const keys = Object.keys(fruits)

keys의 타입은 어떻게 추론될까요? 아마 ('apple' | 'banana' | 'orange')[]가 될거라고 예상할 수 있습니다.

그리고 이 keys를 이용해서 같은 키를 가지면서 값에 ' gift'라는 문자열을 추가하는 fruitGift를 만들어보려고 합니다.

const fruits = {
apple: 'apple',
banana: 'banana',
orange: 'orange'
}

const keys = Object.keys(fruits)

const fruitGift = keys.reduce((result, key) => {
result[key] = fruits[key] + 'gift'
return result
}, {} as typeof fruits)

그러나 keys의 배열의 reduce 함수 내부에서 제공되는 단일 key 값으로는 아래의 에러와 함께 fruits에 접근할 수 없습니다.

Element implicitly has an 'any' type because expression of type 'string' can't be used to index type '{ apple: string; banana: string; orange: string; }'. No index signature with a parameter of type 'string' was found on type '{ apple: string; banana: string; orange: string; }'.

왜냐하면 Object.keys로 얻은 값의 타입추론이 ('apple' | 'banana' | 'orange')[]로 특정된 것이 아니라 string[]로 되기 때문입니다.


Object.keys(object)의 타입추론

Object.keys의 타입정의를 따라가보면 그 이유를 알 수 있습니다.

{
//...
keys(o: object): string[];
//...
}

아마 일반적으로 기대할 수 있는 타입은 keyof typeof o 일텐데요. 아쉽게도 string[]을 반환하는 타입입니다. 따라서 위의 문제처럼 Object.keys의 값들로는 원래의 객체의 값에 액세스 할 수 없었던 것입니다.


왜 string[]을 반환하도록 설계하였을까?

JavaScript에서는 정확한 타입은 런타임을 통해서 결정되기 때문입니다. TypeScript도 결국 JavaScript이기 때문에 그러한 특성을 유지하여 반환값을 string[]로 결정한 것으로 보입니다.

정보

property는 string | number | symbol이 모두 들어올 수 있는데 왜 반환은 string으로만 되나요?
내부적으로 객체는 위 타입으로 들어오는 property 타입을 string으로 처리하기 때문입니다.

예를 들어 설명해보겠습니다. JavaScript의 타입은 런타임에 결정된다는 것을 기억하세요.

// JavaScript

const fruits = {
apple: 'apple',
banana: 'banana',
orange: 'orange'
}

const keys = Object.keys(fruits)

function makeFruitGift() {
return keys.reduce((result, key) => {
// 만약 여기에서 임의로 객체의 속성을 삭제
delete fruits['banana']

result[key] = fruits[key] + 'gift'
return result
}, {})
}

JavaScript에서는 fruits 객체의 속성이 추가 삭제가 매우 자유롭기 때문에 런타임에서 객체가 어떤 모양을 하고 있을지 예측할 수가 없습니다. 예제에서처럼 임의의 타이밍에 객체의 속성하나를 삭제한다고 했을때, fruits[key]의 값이 존재하지 않습니다. 예제에서는 단순히 string이기 때문에 별일이 일어나지 않겠지만, 정상적으로 있어야하는 값이 사라진 상황에서는 어떤 에러가 발생할지 예측할 수 없습니다.

따라서 Object.keys는 그 값으로 어떤 값이 생길지 예측할 수 없기 때문에 string[]을 반환하도록 설계할 수 밖에 없게 됩니다.


의도한대로 타입추론 하기

그러한 작동원리를 이해하더라도 잘 통제된 환경안에서는 적절하게 객체의 키들을 추론하고 싶을 수 있습니다. 아래와 같이 Object.keys를 적절히 타입추론이 가능하도록 wrapping 함수를 만들어 처리하여 사용할 수 있습니다.

type UnknownDictionary = Record<PropertyKey, unknown>

type ObjectKeyOnlyString<Obj extends UnknownDictionary> = Exclude<keyof Obj, symbol | number>

export function objectKeys<Obj extends UnknownDictionary>(obj: Obj) {
return Object.keys(obj) as Array<ObjectKeyOnlyString<Obj>>
}
정보

ObjectKeyOnlyString 부분에서 왜 symbolnumber 타입을 제외하도록 처리하나요?
왜냐하면 Object.keysstring[]을 반환하게 되어있으며 number는 내부적으로 문자열로 변환되어 저장되고, symbol은 열거되지 않기 때문입니다. 여기에서 함수의 목적은 해당 배열의 값을 인자로 들어오는 obj의 키의 문자열이 어떤 것인지 정확히 추론하는 것입니다. 따라서 문자열이 아닌 프로퍼티 타입에 대해서는 제외하도록 처리하였습니다.