일반적으로 Javascript에서 객체 내의 요소들을 다루기 위해서는 Object.keys(targetObject)
를 이용하여 key
배열을 반환한 뒤, 이를 일반적인 배열 다루듯이 다루는 방법을 많이 사용해왔다.
const object1 = {
a: 'somestring',
b: 42,
c: false
};
console.log(Object.keys(object1));
// Expected output: Array ["a", "b", "c"]
그러나 여기에서 Typescript의 특징 중 하나가 문제가 되는데, Typescript에서는 객체의 key를 string이 아니라 string literal
로 간주한다.
const obj = {
foo: "hello",
}
let propertyName = "foo"
console.log(obj[propertyName])
// Element implicitly has an 'any' type because expression of type 'string' can't be used to index type '{ foo: string; }'. No index signature with a parameter of type 'string' was found on type '{ foo: string; }'.(7053)
...네?
이펙티브 타입스크립트 책을 본 사람들이라면 좀 더 잘 알고 있을텐데, string literal은 타입을 선언해준 변수에서 일어나지 않고 타입 추론 을 통해 선언한 변수에서 사용되는 개념이다. 아래 예시 코드를 살펴보자.
const a = "Hello World"
let b = "Hello World"
const c: string = "Hello World"
위의 세 변수 중 c
는 우리가 생각하는 것처럼 string
타입을 가지고, 'Hello World'라는 값을 가진다. b
의 경우는 let
으로 선언되었기 때문에 언제나 재할당의 가능성이 있고, Typescript에서도 이를 고려해 b
의 타입을 string
으로 자동적으로 추론하게 되어 있다.
문제는 a
에서 발생한다. a
는 const
로 선언되어있기 때문에 값의 변경이 없고, Typescript에서는 이러한 값에 대해 좀 더 엄격한 기준을 세우기를 바란다.
그래서 a
의 타입을 일반적인 string
보다 더 좁은 'Hello World'라는 string literal type
으로 추론한 것이다.
Literal Narrowing
이라고 부른다.약간 자기 마음대로인건지 엄격해서 좋은건지...는 모르겠지만,
아무튼 Typescript는 각각의 key를 전부 고유한 단일 타입으로 생각한다는 것이다.
따라서 배열화된 객체 내의 값들을 Object.keys()
를 이용해 조회하는것이 마음대로 안된다. 이 문제를 어떻게 해결하면 좋을까??
다행히 이러한 문제를 해결할 방법이 나와 있다!
바로 Index Signature를 추가하는 것인데, 아래 예시를 살펴보자.
type ObjType = {
[index: string]: string
foo: string
bar: string
}
const obj: ObjType = {
foo: "hello",
bar: "world",
}
const propertyName1 = "foo"
const propertyName2: string = "foo"
console.log(obj[propertyName1]) // ok
console.log(obj[propertyName2]) // ok
위 코드를 보면 객체의 타입을 선언할 때 [index: string]: string
이라는 표현을 추가해준 것을 볼 수 있다. 여기에서 index
는 임의의 단어이기 때문에 사용자가 자유롭게 설정할 수 있다. 이렇게 하면 index
즉 key
의 부분에는 string
타입이 들어가고, value
부분에는 string
이 들어가야함을 명시해주어 이후에 들어오는 값들에 자동적으로 string literal type
이 추론되는 것을 방지할 수 있다.
그렇다면 객체 내 key로 들어갈 요소들을 한정지어줄 수는 없을까? 라는 생각도 해볼 수 있다. 그러나 여기에서는 다른 에러가 발생한다.
type newType = "Hello" | "World"
const newObject = {
[key: newType]: string // An index signature parameter type cannot be a union type. Consider using a mapped object type instead.
}
응애... 저건 왜 또 안되는거야...
알면 알수록 Typescript는 자신만의 세계가 확고한 친구인 것 같다.
그래서 더 공부하는 맛이 좋은 것일지도?
아무튼 에러 메세지만 분석해봐도 답이 나오는데, index signature로 유니온 타입은 사용 불가하다. 이러한 경우 mapped object type
을 사용하라는 메세지를 보내준다.
TS 공식문서를 보면 Mapped Types
를 이렇게 정의하고 있다.
Mapped types build on the syntax for index signatures, which are used to declare the types of properties which have not been declared ahead of time
설명하면 기존의 선언된 속성들의 타입을 요리조리 가공하여 새로운 타입을 반환하는 것을 말한다.
즉, 배열에 map
함수를 적용하듯이 객체 내 속성의 타입들을 가공하기 위해 사용하는 것이다!!
예시를 살펴보자.
type OptionsFlags<Type> = {
[Property in keyof Type]: boolean;
};
type FeatureFlags = {
darkMode: () => void;
newUserProfile: () => void;
};
type FeatureOptions = OptionsFlags<FeatureFlags>;
위 코드에서 OptionsFlags
는 특정 타입을 지정할 수 있는 객체이고, 이때 내부 index signature로 어떤 것이 올지 명확하지 않다(제네릭에 들어가는 Type에 따라 달라지기 때문).
그래서 이에 대한 확장성을 열어두기 위해 Mapped type
을 사용하는데, Property in keyof Type
이라는 구문을 보면 제네릭으로 입력된 객체 타입 내부에 있는 key들을 모두 반환해 타입을 만들어낸다는 것을 의미한다. 이때 FeatureOptions
타입을 아래의 코드와 같다.
type FeatureOptions = {
darkMode: boolean;
newUserProfile: boolean;
}
확실히 유용하다...보면 볼수록 Typescript가 정교한 정적 타이핑을 위해 얼마나 고심하고 노력하고 있는지 알 수 있는 것 같다.