개요
이 글의 제목을 정하는 데 좀 고민이 많이 들었습니다.. 뭔가 복합적인 문제인 듯 한데, 좀 길게 풀어서 설명하자면, const array = ['a', 'b', 'c'] as const
와 같은 readonly 배열에서 'a' | 'b' | 'c'
타입이 아닌 const needle: string = 'needle'
와 같은 string
타입을 이용해 array.includes(needle)
과 같이 사용하고 싶은데, 타입 문제 때문에 그렇지 못해서 수정하고자 쓰는 글입니다.
이 글에서는 타입을 설명할 때 ‘좁다’, ‘넓다’와 같은 말을 쓸 것입니다. 포함된다, 확장된다 라고도 표현할 수 있겠지만, 제 머릿속에서 딱 떠오르지가 않아서요. 헤헤.
이 글은 독자분들이 어느정도 타입스크립트의 기본 개념을 알고 있다는 전제하에 작성되었습니다. 타입을 추론(Inference) 하는 과정을 몸에 좀 익히고 있다면 문제없이 글을 읽으실 수 있을 것입니다.
문제
자 일단, 용어를 어떻게 해야 하는지 좀 고민스러운데, const array, readonly array 등 일맥상통하는 용어들이 있지만, 일단은 readonly 배열이라고 명명하겠습니다. 좀 더 정확히 이야기하면 const
assertions 를 통해서 만든 배열이 readonly 배열이 됩니다. 이 배열은 변경되지 않는 부분을 확실히 함으로써 그 자체로 타입이 됩니다! 바로 아래에서 그 타입을 확인해보세요. vscode 에서 작성하고 변수명에 마우스를 올린다면 실제로 계산된 타입이 보입니다. (주석)
const colors = ['red', 'green', 'blue'] as const; // readonly ["red", "green", "blue"]
type ColorType = typeof colors[number] // "red" | "green" | "blue"
function doWithColor(color: ColorType): void {
// do something
}
ColorType
타입을 이용해서 우리는 각종 함수에 enum
처럼 사용할 수 있습니다. 아주 편리하지요! 하지만 이제 다음과 같은 문제가 생깁니다.
만약 사용자가 어떤 값(userInput
)을 입력해왔다고 합시다. 이 값은 뭐일지 모릅니다. 일단 string
인 건 확실하다고 가정합시다. 하지만 그렇다고 해도 colors
에서의 includes
를 사용할 수는 없습니다. 왜냐하면 colors
에는 애초에 'red'
, 'green'
, 'blue'
만 들어갈 수가 있어서 이것들로만 있는지 없는지 검사할 수 있기 때문입니다!
const userInput: string = getUserInput();
if (colors.includes(userInput)) { // 에러!
// 올바른 userInput 이다!
doWithColor(userInput);
} else {
// 올바르지 않은 userInput 이다!
}
Argument of type 'string' is not assignable to parameter of type '"red" | "green" | "blue"'. ts(2345)
그렇다면, 값은 똑같되 타입만 다른 string
배열을 만들어서 포함되는지 확인하면 되잖아요?
const userInput: string = getUserInput();
const typeCheck: string[] = [...colors];
if (colors.includes(userInput)) {
// 올바른 userInput 이다!
doWithColor(userInput); // 에러!
} else {
// 올바르지 않은 userInput 이다!
}
Argument of type 'string' is not assignable to parameter of type '"red" | "green" | "blue"'.ts(2345)
하지만 어림도 없죠. 에러가 발생합니다. 왜냐하면 여전히 userInput
은 string
이기 때문에, doWithColor(userInput)
입장에서 userInput
은 "red" | "green" | "blue"
뿐 아니라 "purple"
과 같은 이상한 값이 들어올 가능성이 있다고 판단합니다. 저 if
문을 통과했다고 하더라도 말입니다!!
이런 문제를 Type Predicates (타입 서술?)를 활용하여 해결하도록 하겠습니다.
Type Predicates
레퍼런스를 먼저 보는 게 더 마음이 편할 것입니다. 우리는 Type Predicates 를 통해 타입을 좁혀줄 수 있습니다! 자, 타입을 좁힌다는 것이 무슨 뜻일까요? 아래 이미지를 봅시다.
string
과 "red" | "green" | "blue"
를 비교해봅시다. string 은 아주 다양한 것들을 포함할 수 있습니다. 다만 "red" | "green" | "blue"
는 3개 밖에 없습니다. "purple"
은 ColorType
이 될 수 없고, 오직 string
만 가능합니다. 반면 모든 ColorType
은 string
입니다. 직관적으로 표현하자면 string
은 망망대해와 같은 넓고 큰 타입이고, 반면 ColorType
은 가능한 것이 3개 밖에 없는 좁은 타입입니다. 우리가 Type Predicates 를 한다는 뜻은, 본래 string
이었던 userInput
값을, 우리가 직접 검사를 해서 그 값의 타입을 ColorType
으로 간주하게 한다는 것입니다!
타입 Predicates 를 사용하려면 우선 함수를 만들어야 합니다. 그 함수를 user-defined type guard(사용자정의 타입 가드) 라고 명명하겠습니다. 이 함수는 리턴 타입이 boolean
이어야 하는데, boolean
대신 : input is ColorType
과 같이 is
키워드를 넣어서 정의해야 합니다!
export function contains<T extends string>(
list: ReadonlyArray<T>,
value: string,
): value is T {
return list.some((item) => item === value);
}
위 함수는 우선 두 가지의 값을 받습니다. list
는 readonly 배열이고, value
는 검사하고자 하는 값입니다. ColorType
은 string
안에 있는 좁은 타입이므로 string
을 extends
한다고 볼 수 있습니다. 그래서 문제가 없습니다. 그래서 list
안에 하나라도 value
와 일치하는 것이 있다면, true
를 반환하고, 그렇지 않다면 false
를 반환하는 간단한 함수입니다.
이 함수에서 value is T
의 의미는, 이 contains
함수의 결과가 참이라면 value
를 T
타입으로 간주해도 좋다는 뜻입니다. 우리가 실제로 실행할 때에는 T
가 ColorType 으로 inference 되므로, 결과적으로 타입이 좁혀지면서 아래 코드는 문제없이 동작하게 됩니다.
const userInput: string = getUserInput();
const typeCheck: string[] = [...colors];
if (contains(colors, userInput)) {
// 올바른 userInput 이다!
doWithColor(userInput);
} else {
// 올바르지 않은 userInput 이다!
}
활용
필자는 콤마(,
)로 구분된 길다란 문자열을 나눠서 어떤 readonly 배열에 있는 유효한 값들만 모으는 함수를 만들기 위해 이 고생을 했습니다. (매번 선형 탐색을 하기 때문에 커다란 자료를 다룬다면 Set 등의 자료구조를 고려해야 합니다.)
export function contains<T extends string>(
list: ReadonlyArray<T>,
value: string,
): value is T {
return list.some((item) => item === value);
}
export function parseRestrictedArray<T extends string>(
value: string,
list: ReadonlyArray<T>,
): T[] {
const arr = value.split(',');
const result: T[] = [];
arr.forEach((item) => {
if (contains(list, item)) {
result.push(item);
}
});
return result;
}
한계
이 Type Predicates 에서 유의해야 할 점은, 타입을 좁히는 것 밖에 되지 않는다는 점입니다. 타입을 넓히는 것은 as 를 활용한 Type Assertion 을 고려해야 합니다.
마치며
마이크로소프트 타입스크립트 레퍼런스들은 뭔가 빈약한 기분이에요… 있을 거 다 있는 것 같으면서도…