타입 추론(Type Inference)
타입스크립트는 여러 값들의 타입을 자동으로 추론하는 타입 추론기능이 있습니다. 타입스크립트가 코드를 해석하여 해당 값의 타입을 알아서 찾는 편리한 기능입니다.
let a = 3
let b = "타입"
변수 a와 b는 타입을 명시적으로 설정해주지 않았음에도 불구하고 타입스크립트는 a가 number, b가 string이라는 것을 알 수 있습니다. 타입 추론은 변수를 선언하고 초기화할 때 자동으로 일어나는 과정입니다. 변수 뿐만아니라 함수의 반환값이나 객체의 속성에서도 자동으로 타입을 추론하여 해당 값이 어떤 타입인지 알고 있습니다.
Best common type
let x = [0, 1, null];
위와 같이 배열에 숫자와 null이 동시에 들어간 경우는 어떻게 될까요? 타입스크립트는 여러 표현식으로부터 타입 추론을 할 때, 그 표현식들의 타입들을 사용하여 "최적 공통 타입"을 계산합니다. 위 예시에서 x의 타입을 추론하기 위해, 각 배열 요소의 타입을 고려해야 합니다. 여기서 배열의 타입에 대해 number와 null이라는 두 가지 선택지가 제공됩니다. 타입스크립트의 타입 추론 알고리즘은 타입이 될 수 있는 후보를 모두 고려하여 가장 좁은 범위를 가지는 타입을 가장 적절한 타입으로 취급합니다. 따라서 x
는 다음 이미지와 같이 (number | null)[]
타입을 가지게 됩니다.
타입을 가장 좁은 범위로 추론하기 때문에 대게는 현재 사용되는 타입만을 취급하게 됩니다. 대부분의 경우에는 올바르나 그렇지 못한 경우도 있습니다.
let zoo = [new Rhino(), new Elephant(), new Snake()]; // (Rhino | Elephant | Snake)[]
위의 코드에서 zoo는 (Rhino | Elephant | Snake)[]
라는 타입을 가지게 됩니다. 하지만 Rhino
, Elephant
, Snake
가 Animal
의 하위 클래스라면 zoo의 타입은 Animal[]
이 되어야 알맞을 수 있습니다. 이럴때는 명시적으로 타입을 정해주어야 합니다.
let zoo: Animal[] = [new Rhino(), new Elephant(), new Snake()];
문맥상의 타이핑(Contextual Typing)
타입 추론은 때로 타입스크립에서 "반대 방향"으로도 작동합니다. 이를 "문맥상의 타이핑(contextual typing)"이라고 합니다. 문맥적 타이핑은 표현식의 타입이 그 위치에 의해 암시적으로 결정될 때 발생합니다.
document.getElementById("myButton").onclick = function(event) {
console.log(event.target); //<- OK
console.log(event.animal); //<- Error!
};
event.target
은 잘 작동하지만 event.animal
은 에러가 발생합니다. 일반적으로 HTML 요소의 onclick
이벤트 핸들러는 MouseEvent
를 기대합니다. 따라서 event
매개변수는 일반적으로 MouseEvent
타입으로 추론됩니다. event.target
은 MouseEvent
가 가진 속성으로 에러가 없는 반면 event.animal
은 MouseEvent
에는 정의되지 않은 속성이므로 타입스크립트에서는 이를 에러로 처리합니다.document.getElementById("myButton").onclick
에 할당된 함수의 매개변수 event
가 어떤 타입인지 결정하는 기준은 onclick
이벤트 핸들러가 됩니다.
하지만 다음과 같이 어떤 이벤트인지 알 수 없는 경우에는 문맥상의 타이핑이 작동할 수 없습니다. 따라서 event
는 any
타입이 되어 타입스크립트가 개입할 수 없습니다.
const handler = function(event) {
console.log(event.animal); //<- OK
}
Narrowing
Narrowing은 타입스크립트에서 특정 조건을 만족할 때 변수의 타입을 더 구체적인 타입으로 추론하는 과정을 말합니다. 주로 조건문(if 문이나 switch 문 등)을 사용하여 변수의 가능한 값(타입)의 범위를 좁히거나, 타입 가드를 활용하여 변수의 타입을 명시적으로 제한하는 방식으로 이루어집니다. Narrowing은 타입스크립트가 코드를 실행하기 전에 타입을 검사하고, 실행 경로에 따라 변수의 타입을 추론 하고 더 정확한 범위로 좁히는 데 사용됩니다.
let x: number | string;
x = 10;
console.log(typeof x); // "number"
if (typeof x === "number") {
console.log(x.toFixed(2)); // 여기서 x는 number 타입으로 좁혀짐
} else {
console.log(x.toUpperCase()); // 여기서 x는 string 타입으로 좁혀짐
위 코드에서 x
는 초기에 number | string
타입으로 선언되지만 typeof x === "number"
와 같은 조건문을 사용하여 x의 타입을 좁혀나가는 과정을 볼 수 있습니다. typeof x === "number"
조건에서는 x
가 number
타입으로 좁혀지므로 toFixed
메서드를 사용할 수 있습니다. 반대로 else
블록에서는 x
가 string
타입으로 좁혀지며, toUpperCase
메서드를 사용할 수 있습니다.
타입 가드(Type Guards)
타입 가드는 특정 조건을 만족할 때 변수의 타입을 좁히는 기능을 제공합니다. 위 예제에서 typeof x === "number"
는 typeof
연산자를 사용하여 타입 가드를 구현한 것입니다. 이 외에도 사용자 정의 함수, instanceof
연산자, in
연산자 등도 타입 가드로 사용될 수 있습니다.
// 사용자 정의 타입 가드 예시
function isFish(pet: Fish | Bird): pet is Fish {
return (pet as Fish).swim !== undefined;
}
// instanceof를 사용한 타입 가드 예시
if (someValue instanceof MyType) {
// someValue는 MyType 타입으로 좁혀짐
}
// in 연산자를 사용한 타입 가드 예시
if ("swim" in pet) {
// pet은 Fish 타입으로 좁혀짐
}
타입 어설션 (Type Assertion)
가끔씩은 타입스크립트에게 타입을 명확하게 알려주어야 할 상황이 생기기도 합니다. 때때로 변수의 타입을 명확하게 알기 어려운 경우나 변수가 여러 타입을 가질 수 있는 경우 등입니다. 이럴 때 타입 어설션을 사용하여 개발자가 직접 변수의 타입을 지정할 수 있습니다.
async function fetchData() {
try {
const response = await fetch("https://api.example.com/data");
if (!response.ok) {
throw new Error("Failed to fetch data");
}
const data: MyData = await response.json();
// 이후 data 객체를 안전하게 사용
return data;
} catch (error) {
console.error("Error fetching data:", error);
}
}
위와 같은 함수에서 response.json()
은 any를 반환하는 함수입니다. 따라서 해당 함수의 반환값을 MyData로 단정(assertion)해주어야 합니다. const data: MyData = await response.json();
와 같이 사용할 수도 있으며 const data = (await response.json()) as MyData;
와 같이 사용하여도 됩니다. 타입 어설션은 외부 라이브러리 사용이나 HTTP 요청 등 타입스크립트가 타입을 확실하게 알기 어려운 상황에서 주로 사용합니다.
null assertion
때로는 변수가 null
또는 undefined
일 가능성이 있는 경우, non-null 연산자 !
를 사용하여 null
이 아님을 명시적으로 지정할 수 있습니다.
let maybeNullValue: string | null = null;
// maybeNullValue가 null일 가능성이 있는데도 !를 사용하여 타입을 명시적으로 지정
const definiteValue: string = maybeNullValue!;
프로그램의 구조 상 개발자는 해당 값이 절대 null이 될 수 없음을 알고 있지만 타입스크립트는 이를 알 수 없는 경우가 많습니다. 이때 non-nill 연산자를 활용합니다.
주의 사항
타입 가드 없이 as
나 !
연산자를 남발하는 것은 코드의 타입 안정성을 저하시킬 수 있습니다. 가능하면 타입 가드를 사용하여 실행 시점의 조건을 통해 변수의 타입을 좁히는 방식으로 타입스크립트의 강력한 타입 추론 기능을 활용하는 것이 좋습니다. as
나 !
연산자는 타입스크립트가 제공하는 타입 시스템을 보완하는 도구로, 필요한 경우에만 명시적으로 사용하는 것이 좋습니다.
더 알아보기: Array.prototype.pop의 반환값에는 항상 undefined가 포함된다?!
가끔은 타입가드를 통해 타입을 좁혀주었다고 생각하여도 그렇지 못한 경우도 있습니다.
const array: number[] = [1, 2, 3];
let num: number;
if (array.length !== 0) {
num = array.pop(); // error
}
Array.prototype.pop
메서드는 배열이 비어있다면 undefined
를 반환하기 때문에 기본적으로 배열 내부의 요소 | undefined
로 타이핑됩니다. 하지만 위의 코드에서 array.length !== 0
를 통해 배열이 비어있지 않는 것을 확실하게 하였지만, pop
이 반환하는 값은 여전히 배열 내부의 요소 | undefined
로 에러가 발생합니다. 이는 TypeScript가 올바르게 추적하기 힘든 문제로 인해 두고 있다고 하며, 편하게는 !
를 통한 타입 어설션을 통해 에러를 없애는 방법(권장되지는 않음, 확실히 undefined가 아님을 보장할 수 있는 경우에 사용)을 사용하여야 합니다. 권장되는 방법은 다음과 같습니다.
if (array.length !== 0) {
const a = array.pop();
if (a !== undefined) {
num = a; // ok!
} else {
// ...
}
}
해결법은 미리 알아보았으니 이 문제가 발생하는 원인도 알아보겠습니다. 어떤 경우에 배열의 길이가 0이 아님에도 undefined
가 반환될 수 있을까요? 정답은 "없습니다."입니다. pop
메서드는 배열의 길이가 0일 때만 undefined
를 반환합니다. 다만 앞서 말했듯이 타입스크립트가 이를 올바르게 추적할 수 없기 때문에 발생한 문제입니다. 타입스크립트에서 pop
메서드를 사용할 때마다 해당 배열의 길이가 정확히 얼마가 되는 지 확인할 길이 없습니다.
import axios from "axios";
async function fetchData(): Promise<void> {
try {
const response = await axios.get("https://api.example.com/data");
const data = response.data as number[];
용;
console.log(data);
} catch (error) {
console.error("Error fetching data:", error);
}
}
위의 코드처럼 HTTP 요청이 숫자로 이루어진 배열입니다. 그러나 이 배열의 길이를 확인할 방법은 없습니다. 타입스크립트 뿐만 아니라 개발자조차도말입니다. 그래서 타입스크립트의 개발자들은 pop
메서드는 배열의 길이와 상관없이 undefined
도 함께 반환하는 것이 더 올바르다고 생각한 것 같습니다. 추가적으로 이런 사례를 지원하기 위한 알고리즘의 변경의 비용이 엄청 클 것입니다. 자세한 내용은 참고 자료에 추가한 링크에서 확인해 볼 수 있습니다.
참고 자료
TypScript Docs, Type Inference
TypeScript GitHub, Issues #30406
stackoverflow
'개발 > TypeScript' 카테고리의 다른 글
타입스크립트 심층 분석 2 - 타입 추론, 특수 타입, const와 let 키워드 차이(+참조값 및 원시값)에 따른 타입 추론 (0) | 2024.08.12 |
---|---|
type-challenges 문제 풀이로 타입스크립트 타입 시스템 깊이 파기: Easy 난이도 - 2 (0) | 2024.08.05 |
타입스크립트 심층 분석 1 - 타입스크립트의 역할 및 타입스크립트의 뿌리, 타입(Type) 이해하기 (0) | 2024.08.04 |
type-challenges 문제 풀이로 타입스크립트 타입 시스템 깊이 파기: Easy 난이도 - 1 (0) | 2024.07.12 |
개발 노트: React navigation에서 덜 귀찮기 위해 더 강력한 타입 사용하기 (0) | 2024.03.27 |