React(CRA), TypeScript 기반의 프로젝트에서 객체의 프로퍼티를 읽다가 만난 컴파일 에러다.
TypeScript는 기본적으로 객체의 프로퍼티를 읽을 때, string
타입의 key 사용을 허용하지 않는다. 아래의 코드에서 에러가 발생한 이유는 string literal
타입만 허용되는 곳에 string 타입을 사용했기 때문이다.
// 객체를 기본값으로 가진 recoil atom 생성
const chartFilterAtom = atom({
key: 'chartFliterAtom',
default: {
chatCount: true,
joinCount: true,
likeCount: true,
joinAndLeave: true,
chatTotal: true,
},
});
// atom 상태를 변경할 수 있는 setChartFilter 함수에서 이전 상태를 filters라는 인자로 전달 받아 string 타입의 key로 객체 프로퍼티 접근 시 에러 발생
const [chartFilter, setChartFilter] = useRecoilState(chartFilterAtom);
setChartFilter(filters => {
for(const key in filters) {
console.log(`${filters[key]}`) // error !
}
})
아래 코드처럼 객체에 index signature
를 추가함으로써 string타입과 literal type 모두를 사용해서 객체에 접근할 수 있다.
아래처럼 index signature가 선언된 경우 모든 프로퍼티가 해당 타입에 속해야 한다.
interface IChartFilter {
[key: string]: boolean // index signature 추가, key는 정해진 키워드가 아니며 변경 가능하다.
chatCount: boolean
joinCount: boolean
likeCount: boolean
joinAndLeave: boolean
chatTotal: boolean
// Error case
iAmWrongType: number // error! Property 'iAmWrongType' of type 'number' is not assignable to string index type 'boolean'.
}
const chartFilterAtom = atom<IChartFilter>({
key: 'chartFliterAtom',
default: {
chatCount: true,
joinCount: true,
likeCount: true,
joinAndLeave: true,
chatTotal: true,
},
})
const [chartFilter, setChartFilter] = useRecoilState(chartFilterAtom);
setChartFilter(filters => {
for(const key in filters) {
console.log(`${filters[key]}`) // ok
}
})
아래의 TypeScript 코드에서 b
와 c
는 string 타입이지만 a
는 "Hello World"
타입이다.
b
변수는 let으로 선언되어 재할당이 가능해 어떤 문자열이든 넣을 수 있기에 컴파일러는 이 변수를 string 타입으로 추론한다. 그리고 c
변수는 명시적으로 string 타입으로 선언했기에 string 타입이다.
const a = "Hello World"
let b = "Hello World"
const c: string = "Hello World"
하지만 a의 경우 컴파일러는 string이 아닌 더 좁은 타입(narrowed type)으로 선언한 것으로 추론한다. 이것을 Literal Narrowing이라고 한다.
따라서 a
의 타입을 string이 아닌 string타입을 좁혀 만든 string litreal type
이다. 여기서 '좁힌다'의 의미는 무한대의 경우의 수를 가질 수 있는 string 타입보다 훨씬 구체적인 string의 부분 집합, "Hello World"
만을 허용하는 타입을 선언했다는 뜻이다.
아래와 같이 명시적으로 literal type을 선언하면 let으로 선언된 변수도 "Hello World"
타입만을 허용하도록 만들 수 있다.
type HelloWorldType = "Hello World" // literal type
let a: HelloWorldType = "Hello World" // ok
a = "hahaha" // compile error: "hahaha"는 "Hello World"가 아니기 때문.
string literal 타입은 열거형 타입처럼 사용할 때 매우 유용하다. 예를 들어 마우스 이벤트를 처리하는 함수가 있다고 하자. 마우스 이벤트의 종류는 이미 정해져 있을 것이다. JavaScript의 방법대로 이벤트 이름을 string
타입으로 받을 수 있다. 하지만 오타 혹은 유효하지 않은 이벤트 이름으로 인해 발생하는 런타임에러를 사전에 방지할 수 없다.
function handleEvent(event: string) {}
handleEvent("click")
handleEvent("clock") // compile error: 오타. 컴파일 타임에 발견할 수 없다.
handleEvent("hover") // compile error: 유효하지 않은 이벤트 이름. 마찬가지로 컴파일 타임에 걸러낼 수 없다.
다음의 예제과 같이 string literal 타입 조합만을 허용하도록 하도록 수정한다. 여기서 |
은 union type을 의미하며 두 개의 타입 이상을 결합할 수 있다.
type EventType = "mouseout" | "mouseover" | "click"
function handleEvent(event: EventType) {}
handleEvent("click")
handleEvent("hover") // compile error: Argument of type '"hover"' is not assignable to parameter of type 'EventType'.
이렇게 string literal 타입을 활용하면 "clock"
과 어이 없는 오타를 컴파일 타임에 알 수 있으며, IDE에서 제공하는 suggestion 기능(ctrl + space) 편리함을 누릴 수도 있다.
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 signature를 추가함으로써 string타입과 literal type 모두를 사용해 객체에 접근할 수 있게 되었다. 참고로 index signature가 선언된 경우 모든 프로퍼티가 규칙을 따라야 하며 그렇지 않으면 에러 가 발생한다.
type ObjType = {
[key: string]: string
foo: string
bar: number // error! Property 'bar' of type 'number' is not assignable to string index type 'string'.
}