타입스크립트는 자바스크립트 위에 타입시스템을 추가하여 코드의 안정성을 높이고 대규모 애플리케이션 개발을 보다 용이하게 하려는 목적으로 만들어졌습니다. 자바스크립트는 동적 타입 언어로 런타입에 타입과 관련된 오류거 발생할 수 있습니다. 이에 마이크로소프트 사에서 자바스크립트의 한계, 특히 코드의 안정성과 유지 보수성 향상을 위해 타입스크립트를 개발하였습니다. 타입스크립트는 컴파일 과정을 통해 자바스크립트 코드로 변환되는데, 컴파일 시에 타입 오류를 발견할 수 있도록 만들었습니다. 더불어 자바스크립트에서 지원이 미흡했던 객체 지향 프로그래밍을 쉽게 구현할 수 있도록 하였습니다.
타입 추론
타입스크립트의 이름에서 드러나있듯이 타입은 타입스크립트에서 가장 중요한 요소입니다. 특히 타입스크립트는 자바처럼 모든 변수에 대해 타입을 선언해줄 필요가 없이 자동으로 타입을 추론하여 해당 변수나 값이 어떤 타입을 가지는지 꽤 정확하게 추론합니다. 타입스크립트는(타입을 명시적으로 선언하지 않을 경우) 기본적으로 값에 기반하여 타입을 추론합니다. 변수에 초기값이 할당되면, 타입스크립트는 그 값을 기반으로 타입을 추론하고 결정합니다.
let myNumber = 1; // number
let myString = "string"; // string
let myStringAndNumberArray = ["1", 2]; // (string | number)[]
let myNaNIsNumber = NaN; // number
따로 타입을 지정하지 않았을 경우 자바스크립트의 기본 자료형에 따로 타입을 추론합니다.
타입스크립트의 타입 추론 기준
타입스크립트가 타입을 추론할 때는 해당 값을 포함하는 가장 좁은 범위의 타입을 선택하여 변수를 추론합니다. 해당 값에 대한 타입을 가장 구체적으로 추론하기 위해서입니다. 예를 들어 This is string
이라는 문자열을 할당할 수 있는 타입은 string
과 any
가 있습니다. 여기에 string
을 포함한 유니온 타입까지 추가되면 수 없이 많은 타입에 해당 문자열을 할당할 수 있습니다. 타입스크립트는 여기서 가장 좁은 타입인 string
을 해당 문자열의 타입으로 지정합니다.
여기서 This is string을 리터럴 타입에 할당한다면 더 좁은 범위의 타입에 할당할 수는 있습니다. 하지만 유연성이 크게 제한되기 때문에 string으로 추론합니다. 만약 리터럴 타입으로 추론한다면 코드 작성이 엄청 불편해질 것입니다.
타입은 집합이(아니)다.
타입스크립트가 타입을 추론할 때 어떻게 해당 값이 다른 타입이 되는지 알 수 있을까요? 타입은 메모리에 존재하는 값과 속성을 나타낸 것입니다. 그리고 타입 간에는 포함되는 관계가 있을 수 있습니다. 하나의 타입(타입 A)이 가진 모든 속성을 다른 타입(타입 B)이 가질 수 있다면 타입 A는 타입 B의 서브타입이 됩니다. 이는 집합과 거의 같습니다.
타입이 집합과 비슷하게 해석될 수 있지만 엄밀히 따지면 타입을 집합이라고 하는 것은 옳지 않습니다. 집합은 수학용어고 타입은 프로그래밍 용어라서 그런 것이 아닙니다.(chat GPT는 이렇게 대답합니다...) 쉽게 설명하면 집합은 "조건에 부합하는 객체들의 모임"이고 타입은 앞에서 언급한 "조건"이라고 할 수 있습니다. 그렇다고 타입이 문법을 의미하는 것은 아니며 객체를 어떻게 구성하고 정의하는 지에 관한 것입니다. 즉 타입을 통해 정의된 객체의 모임이 집합입니다. 다만 타입스크립트를 이해하는 데 도움이 되기 때문에 집합처럼 설명을 하겠습다. 보다 자세한 내용은 여기를 참고하세요.
type Animal = {
name: string;
}
type Dog = {
name: string;
bark(): void;
}
name
속성만 가진다면 Animal
타입이 될 수 있습니다. 하지만 Dog
가 되기 위해서는 name
속성과 bark
메서드를 가지고 있어야 합니다. 그렇다면 Dog
는 name
을 가져야만 합니다. 따라서 모든 Dog
는 Animal
의 서브타입이 됩니다. 그리고 Animal
은 Dog
의 슈퍼타입입니다.
여기서 모든 Dog
는 Animal
의 속성을 가지기 때문에 다음과 같이 사용할 수 있습니다.
function greetAnimal(animal: Animal) {
console.log(`Hello, ${animal.name}`);
}
const myDog: Dog = {
name: "흰둥이",
bark() {
console.log("멍멍");
},
};
// Dog는 Animal의 서브타입이므로, greetAnimal 함수에 전달할 수 있습니다.
greetAnimal(myDog);
Animal
이 가지는 모든 속성을 Animal
이 가지기 때문에 greetAnimal
에서 인자의 속성에 접근할 때 안전하게 접근할 수 있습니다.
집합으로 추론하기
타입스크립트는 위와 같은 방식으로 집합을 이용해서 타입을 추론합니다. 만족하는 타입 중 가장 좁은 타입을 찾게 되는 것입니다. 값이 들어갈 수 있는 타입이 여러 개라면 가장 구체적인 서브타입을 찾는 것입니다. 그리고 어떤 타입이 다른 타입의 서브타입인지 알기 위해서는 어떤 타입의 속성을 다른 타입이 전부 가졌는지 확인하면 됩니다. 물론 서로가 서로의 서브타입이 되는 것도 가능합니다.
type Computer = {
cpu: string;
ram: string;
price?: number;
};
type Laptop = {
cpu: string;
ram: string;
screen?: string;
};
Laptop
이 Computer
의 서브타입인지 알아보기 위해서는 Laptop
이 Computer
의 필수 속성인 cpu
, ram
을 string
으로 가지고 선택속성인 price
는 stirng
으로 가지거나 가지지 않는 지 확인하면 됩니다. 위의 예시에서 Laptop
은 필수 속성 cpu
와 ram
을 가지고 있으면 똑같은 string
입니다. 그리고 price
도 undefined
로 조건에 만족하기 때문에 Laptop
은 Computer
의 서브타입이 됩니다.
여기서 특이한 점은 Laptop
의 screen
속성도 선택 속성이기 때문에 서로가 서로의 서브타입이자 슈퍼타입이 됩니다. 여기서 screen
이 필수 속성이었다면 Laptop
은 Computer
의 서브타입이고 반대는 성립하지 않습니다.
특수 타입과 타입 집합
특수 타입
anyany
타입은 타입스크립트의 타입 검사에서 완전히 벗어나는 것을 의미합니다. any
타입을 사용하는 변수는 어떤 타입의 값도 가질 수 있으며, 타입 검사를 우회할 수 있습니다. 그러나 이로 인해 코드의 타입 안전성이 낮아지므로, any
를 남용하는 것은 되도록 피해야 합니다. 대신 unknown
또는 구체적인 타입을 사용하는 것이 권장됩니다.
void
함수가 값을 반환하지 않음을 나타냅니다. 반환값이 없거나 명시적으로 반환값을 제공하지 않는 함수에 사용됩니다. undefined
와 다른 점은 void
는 변수에 할당될 수 없다는 것입니다. 함수의 반환값에서만 사용되며, 해당 함수가 반환하는 값을 사용하지 않겠다는 의미로 타입스크립트는 이 타입을 반환하는 함수가 무엇을 반환하든 상관하지 않습니다.
unknownany
와 비슷하지만, 좀 더 안전한 타입입니다. unknown
은 단어 그대로 어떤 타입인지 알 수 없기 때문에 타입 검사를 수행하여야 한다는 뜻입니다. 그래서 unknown
타입의 변수에 접근하려면 먼저 타입 검사를 수행해야 합니다. 이 타입은 코드가 어떤 값을 포함할지 모를 때 사용되며, 타입 안전성을 보장하려면 실제 타입을 확인하고 나서 작업을 수행해야 합니다.
never
never
는 실제 코드에서의 사용은 함수 혹은 연산에서 반환이 절대 발생하지 않는 경우, 그리고 이 타입이 특정 조건에 맞는 값을 절대 가질 수 없다는 것을 알릴 경우에 사용합니다. 함수 혹은 연산에서 반환이 절대 발생하지 않는 경우라는 것은 undefined
가 반환되는(실제로 반환되는 값이 없음) 경우와는 달리 반환이라는 사건조차 일어나지 못하는 상황을 의미합니다. 실제 코드에서 함수가 정상적으로 종료되지 않거나, 예외를 던지거나, 무한 루프에 빠지는 경우가 해당됩니다. 반면 void
는 함수가 정상적으로 실행 완료되었지만 반환하는 값이 없다는 것을 의미합니다.
일반적인 상황에선 거의 쓰이지 않습니다.
제네릭을 만들 때 never을 반환하도록 하면 특정 조건에 만족하지 않음을 알려 타입을 반환하지 않는 다는 뜻입니다. undefined를 반환하는 것이 아닌 해당 조건의 경우는 사용하지 않겠다는 말과 같습니다.
특수 타입의 집합 포함 관계
참고: strict 모드 기준의 설명입니다.
설명에서 알 수 있듯이 unknown
은 가장 넓은 타입입니다. 모든 타입이 unknown
에 할당 될 수 있습니다. 그리고 never
은 가장 좁은 타입입니다. 어떤 타입에도 대입할 수 없는 타입입니다. any
는 모든 타입의 슈퍼타입이자 서브타입입니다. 따라서 모든 타입을 할당할 수 있으며 모든 타입에 any
를 할당할 수 있습니다. void
는 undefined
의 슈퍼타입이며 undefined
을 제외한 그 어떤 타입과도 관계를 맺지 않습니다. never
는 모든 타입의 서브 타입이 되며, 원칙적으로 never
값은 발생하지 않는 상황을 가리킵니다.
let과 const에 따른 다른 타입 추론
let
과 const
키워드에 따라서 타입스크립트는 조금 다르게 타입을 추론합니다. const
로 선언된 변수는 값을 변경할 수 없기 때문에 해당 값의 타입을 리터럴 타입으로 추론하게 됩니다. 값이 바뀌지 않기 때문에 더 정확하게 해당 값을 추론하려고 하기 때문입니다. 반면 let
으로 변수를 선언한다면 값이 바뀔 수 있기 때문에 더 일반적인 타입으로 추론합니다. 인스턴스라면 원본이 되는 class를 타입으로 추론하고 문자열은 모두 string
으로 추론합니다.
원시값
const constTrue = true; // true
let letTrue = true; // boolean
const constString = "This is String"; // "This is String"
let letString = "This is String"; // string
참조값
그러나 참조값일 경우에는 조금 다릅니다. const
로 선언된 배열일지라도 배열의 요소는 변할 수 있기 때문에 let
과 cosnt
의 차이가 적어집니다.
const constArray = ["1", 2]; // (string | number)[]
let letArray = ["1", 2]; // (string | number)[]
let 키워드 타입 추적
let 키워드의 경우 타입을 함께 선언하지 않아 최초에 any
로 선언되었다면 코드의 흐름에 따라 타입을 재추론하기도 합니다.
let letKeyword;
letKeyword = [];
letKeyword.push(""); // any[]
letKeyword = "dwa";
letKeyword.charAt(2); // string
참고 자료
NAVER ENGINEERING DAY 2024, infer, never만 보면 두려워지는 당신을 위한 타입 추론 - Naver D2에서 보기
TypeScript, New 'unknown' top type
stackoverflow, 'unknown' vs. 'any'
'개발 > TypeScript' 카테고리의 다른 글
type-challenges 문제 풀이로 타입스크립트 타입 시스템 깊이 파기: Easy 난이도 - 3 (0) | 2024.08.23 |
---|---|
타입스크립트 심층 분석 3 - 타입스크립트의 구조적 타이핑과 타입호환성 (0) | 2024.08.21 |
type-challenges 문제 풀이로 타입스크립트 타입 시스템 깊이 파기: Easy 난이도 - 2 (0) | 2024.08.05 |
타입스크립트 심층 분석 1 - 타입스크립트의 역할 및 타입스크립트의 뿌리, 타입(Type) 이해하기 (0) | 2024.08.04 |
type-challenges 문제 풀이로 타입스크립트 타입 시스템 깊이 파기: Easy 난이도 - 1 (0) | 2024.07.12 |