Typescript 에서 객체 내 요소 확인하기

오형근·2023년 3월 9일
1

Typescript

목록 보기
13/15
post-thumbnail

우리 아주 귀엽고 엄격한 Typescript

일반적으로 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이요?

이펙티브 타입스크립트 책을 본 사람들이라면 좀 더 잘 알고 있을텐데, 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에서 발생한다. aconst로 선언되어있기 때문에 값의 변경이 없고, Typescript에서는 이러한 값에 대해 좀 더 엄격한 기준을 세우기를 바란다.
그래서 a의 타입을 일반적인 string보다 더 좁은 'Hello World'라는 string literal type으로 추론한 것이다.

이러한 현상을 Literal Narrowing이라고 부른다.

약간 자기 마음대로인건지 엄격해서 좋은건지...는 모르겠지만,
아무튼 Typescript는 각각의 key를 전부 고유한 단일 타입으로 생각한다는 것이다.
따라서 배열화된 객체 내의 값들을 Object.keys()를 이용해 조회하는것이 마음대로 안된다. 이 문제를 어떻게 해결하면 좋을까??

Index Signature 선언하기

다행히 이러한 문제를 해결할 방법이 나와 있다!

바로 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는 임의의 단어이기 때문에 사용자가 자유롭게 설정할 수 있다. 이렇게 하면 indexkey의 부분에는 string 타입이 들어가고, value 부분에는 string이 들어가야함을 명시해주어 이후에 들어오는 값들에 자동적으로 string literal type이 추론되는 것을 방지할 수 있다.

그렇다면 객체 내 key로 들어갈 요소들을 한정지어줄 수는 없을까? 라는 생각도 해볼 수 있다. 그러나 여기에서는 다른 에러가 발생한다.

또 그냥은 안 넘어가는 TS씨

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을 사용하라는 메세지를 보내준다.

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가 정교한 정적 타이핑을 위해 얼마나 고심하고 노력하고 있는지 알 수 있는 것 같다.


해당 글은 아래 링크를 참고하여 만들어졌습니다.

TypeScript에서 string key로 객체에 접근하기
Mapped Types(TS 공식문서)

0개의 댓글