왜 Object.keys는 string[] 타입을 리턴하나요?

camel·2024년 11월 24일
9
post-thumbnail

✅ 개요

타입스크립트에서는 타입 추론을 활용하는 것이 권장되는 좋은 방법입니다.

이러한 맥락에서 타입 단언(assertion)의 사용을 최소화하려 노력하던 중, Object.keys()의 반환 타입이 string[]인 점에 대해 의문이 생겼습니다. 처음에는 Object.keys()가 객체의 키들을 반환하므로 keyof 타입의 배열을 반환하는 것이 더 적절하지 않을까 생각했습니다.

하지만 이 문제를 공부해본 결과, 타입스크립트의 핵심 디자인 목표인 "JavaScript 코드의 런타임 동작 보존"이라는 관점에서 string[]이 더 적합한 타입이라고 생각하게 되었습니다.

이 글에서는 이러한 결론에 도달한 과정과 그 내용들에 관해 설명하고자 합니다.

💡 관련된 추가 내용들은 문서 하단에서 확인하실 수 있습니다.


👀 Object.keys 타입

먼저 제가 궁금했던 상황을 코드로 살펴보겠습니다.

const a: {} = {};
const b: object = {};
const c: {x:string, y:number} = { x: '', y: 2 };

Object.keys(a) // string[]
Object.keys(b) // string[]
Object.keys(c) // string[]

위 예시처럼 객체의 타입과 관계없이 Object.keys()는 항상 string[]을 반환합니다. 이러한 타입 정의가 어떻게 이루어졌는지 타입스크립트 라이브러리 파일을 통해 확인해보았습니다.

타입스크립트의 tsconfig에서는 lib 옵션을 통해 사용할 라이브러리를 지정할 수 있으며, 각 라이브러리마다 타입 정의 방식이 조금씩 다릅니다. Object.keys()의 경우 두 개의 라이브러리 파일에서 다음과 같이 정의되어 있었습니다:

// lib.es5.d.ts
keys(o: object): string[];

// lib.es2015.d.ts
keys(o: {}): string[];

매개변수의 타입은 다르지만, 두 정의 모두 반환 타입이 string[]로 동일합니다. 즉, 어떤 객체를 인자로 전달하더라도 항상 문자열 배열을 반환하도록 설계되어 있습니다.

이러한 타입 정의 확인한 후, 제가 대안으로 생각했던 keyof 타입에 대해 알아보도록 하겠습니다.

🔑 keyof 타입과의 비교

타입스크립트의 keyof 연산자는 객체 타입에서 키들의 유니온 타입을 추출합니다. Object.keys()의 반환 타입과 비교해보기 위해 앞선 예제에 keyof를 적용해보겠습니다:

const a: {} = {};
const b: object = {};
const c: {x:string, y:number} = { x: '', y: 2 };

Object.keys(a) // string[]
type AKeys = Array<keyof typeof a>; // never[]

Object.keys(b) // string[]
type BKeys = Array<keyof typeof b>; // never[]

Object.keys(c) // string[]
type CKeys = Array<keyof typeof c>; // ("x" | "y")[]

keyof를 사용했을 때, 각 객체의 타입에 따라 더 구체적인 타입 정보를 얻을 수 있습니다. 특히 c 객체의 경우 키들이 정확히 "x" | "y" 유니온 타입의 배열로 추론됩니다.

처음에는 이처럼 더 정확한 타입 정보를 제공하는 keyof 방식이 Object.keys()의 반환 타입으로 더 적절해 보였습니다.

이러한 의문을 해결하기 위해 검색을 통해 더 많은 내용들을 찾을 수 있었습니다.


❗ 타입스크립트 개발팀의 의도

타입스크립트 개발팀의 공식 입장을 확인하기 위해 GitHub 이슈들을 살펴보았습니다. 그 중 특히 주목할 만한 것은 "Object.keys의 반환 타입이 잘못되었다"는 이슈(#12870)에 대한 기여자의 답변이었습니다:
GitHub 이슈 스크린샷

"이는 의도된 동작입니다. 타입스크립트의 타입들은 열린 구조(open ended)입니다. 따라서 keyof가 반환하는 것은 런타임에서 실제로 존재할 수 있는 모든 속성들의 부분집합일 수 있습니다."

이 답변에서 중요한 개념은 "열린 구조"입니다. 열린 구조란 타입이 명시적으로 선언된 속성 외에도 추가적인 속성을 가질 수 있음을 의미합니다.

이러한 설계 철학을 더 깊이 이해하기 위해서는 타입스크립트의 핵심 개념인 "duck typing" 또는 "structural typing"에 대해 알아볼 필요가 있습니다.
이는 타입스크립트가 채택하고 있는 Structural Type System의 근간이 핵심 개념입니다.

🔍 Structural Type System

타입스크립트의 핵심 원칙 중 하나는 타입 검사가 값의 구조(structure)에 기반한다는 점입니다. 실제 예시를 통해 살펴보겠습니다:

interface Person {
    name: string;
    age: number;
}

const person: Person = {
    name: "Kim",
    age: 30,
}

const personKeys = Object.keys(person) // string[]
type PersonKeys = Array<keyof typeof person>; // (keyof Person)[]

하지만 타입스크립트의 구조적 타이핑(Structural Typing)으로 인해, 런타임에서는 정의된 타입보다 더 많은 속성을 가질 수 있습니다:

interface Person {
    name: string;
    age: number;
}

const fullPerson = {
    name: 'Kim',
    age: 30,
    address: 'Seoul',
    phone: '010-1234-5678',
};

const person2: Person = fullPerson; // 정상 동작
const person2Keys = Object.keys(person2) // string[]
type Person2Keys = Array<keyof typeof person2>; // (keyof Person)[]

위 예시에서 fullPersonPerson 인터페이스에 정의되지 않은 추가 속성(address, phone)을 가지고 있지만, 구조적 타이핑에 의해 Person 타입으로 할당이 가능합니다.
런타임에서 Object.keys(person2)는 이러한 추가 속성들도 포함하게 됩니다.

이것이 바로 Object.keysstring[]을 반환하는 이유입니다.
컴파일 시점의 타입 정보(keyof)보다 런타임에 실제로 존재할 수 있는 속성이 더 많을 수 있기 때문입니다.

여기서 흥미로운 의문이 하나 생깁니다: 객체의 키로 string뿐만 아니라 symbol도 사용할 수 있는데, 왜 Object.keyssymbol 키를 반환하지 않을까요?

🤔 객체의 키 타입과 Object.keys()

자바스크립트에서 객체의 키는 symbol을 제외한 모든 타입이 문자열로 자동 변환됩니다:

  • 숫자 → 문자열 (예: 1"1")
  • 불리언 → 문자열 (예: true"true")
  • null"null"
  • undefined"undefined"
  • 객체 → 문자열 (toString() 메서드 호출 결과)

따라서 객체의 키로 실제 사용 가능한 타입은 stringsymbol 뿐입니다. 그런데 Object.keys()는 왜 symbol 타입의 키를 반환하지 않을까요?

이는 symbol이 ES2015(ES6)에서 도입될 때의 설계 의도 때문입니다. symbol 키는 일반적인 열거 메서드에서 제외되도록 설계되었으며, 대신 전용 메서드를 통해 접근해야 합니다:

const sym = Symbol('mySymbol');
const obj = {
  normalKey: 'value',
  [sym]: 'symbol value'
};

console.log(Object.keys(obj)); // ['normalKey']
console.log(Object.getOwnPropertySymbols(obj)); // [Symbol(mySymbol)]

// 모든 키를 조회하려면
console.log(Reflect.ownKeys(obj)); // ['normalKey', Symbol(mySymbol)]

이처럼 symbol 키에 접근하기 위해서는 Object.getOwnPropertySymbols()를 사용하거나, 모든 키를 조회하고 싶다면 Reflect.ownKeys()를 사용할 수 있습니다.


💡 Object.keys의 실용적인 활용 방법

지금까지의 분석을 바탕으로, Object.keys를 실제로 어떻게 활용할 수 있는지 살펴보겠습니다. Stack Overflow에서는 주로 다음 두 가지 접근 방법이 권장되고 있습니다:

1️⃣ 타입 가드 사용

function isKeyOf<T extends object>(obj: T): (key: PropertyKey) => key is keyof T {
    return (key: PropertyKey): key is keyof T => key in obj;
}

const obj = { a: 1, b: 2 };
const keys = Object.keys(obj).filter(isKeyOf(obj)); // ('a' | 'b')[]

이 방법은 in 연산자를 활용해 런타임에 키의 존재 여부를 검증합니다.
타입 단언보다 안전한 방법으로, 실제 존재하지 않는 키에 대한 접근을 방지할 수 있습니다.

2️⃣ 타입 단언 사용

const getKeys = <T>(obj: T) => Object.keys(obj) as Array<keyof T>
const obj = {a: 1, b: 2}
const objKeys = getKeys(obj) // ("a" | "b")[]

이 방법은 제네릭 유틸리티 함수와 타입 단언을 사용하는 접근법입니다.
하지만 주의할 점이 있습니다:

  1. 타입 단언(as)은 타입스크립트의 타입 체크를 우회하므로 타입 안전성을 보장할 수 없습니다.
  2. 구조적 타이핑으로 인해 예상하지 못한 속성이 포함될 수 있어 런타임에서 예기치 않은 동작이 발생할 수 있습니다.

따라서:

  • 타입 안전성이 중요한 경우에는 타입 가드나 타입 좁히기를 사용하는 것이 좋습니다.
  • 타입 단언은 타입이 확실히 보장되는 간단한 상황이나 불가피한 경우에만 제한적으로 사용하는 것이 좋습니다.

🙌 끝으로

Object.keys를 통해 타입스크립트에 대해 깊이 있게 탐구하면서 많은 것을 배웠습니다.

타입스크립트의 디자인 원칙과 FAQ를 살펴보며 이 언어가 추구하는 방향에 관해 알 수 있었습니다.
앞으로 라이브러리나 프로그래밍 언어를 공부할 때는 공식 문서, 특히 디자인 목표를 꼭 찾아서 보는 것이 중요하다고 느끼게 되었습니다.

이 과정을 거치면서 타입스크립트에 대해 더 배울 수 있어서 좋은 경험이었습니다.

감사합니다.


출처:
[공식 문서/스펙]
타입스크립트 디자인 목표
https://github.com/microsoft/TypeScript/wiki/TypeScript-Design-Goals
excess-property-checks
https://www.typescriptlang.org/docs/handbook/2/objects.html#excess-property-checks
Object key
https://tc39.es/ecma262/#sec-topropertykey

[관련 이슈/토론]
Object.keys has wrong return type from 2.1 #12870
https://github.com/Microsoft/TypeScript/issues/12870
Exact Types(open)
https://github.com/microsoft/TypeScript/issues/12936
Redefine Object.keys to use keyof
https://github.com/microsoft/TypeScript/issues/20503
typescript-object-keys-return-string
https://stackoverflow.com/questions/52856496/typescript-object-keys-return-string
Object.keys() types refinement, and Object.entries() types bugfix
https://github.com/microsoft/TypeScript/pull/12253#issuecomment-263132208

profile
잘부탁드립니다.

2개의 댓글

comment-user-thumbnail
2024년 12월 22일

너무 좋은 글이네요. 감사합니다.

1개의 답글

관련 채용 정보