타입 추론은 타입스크립트에서 개발자가 별도로 타입을 지정하지 않아도 타입스크립트가 해당 변수의 초기화에 사용된 값에 따라 자동으로 타입을 결정하는 기능이다.
const a = 1;
a = '123'; // 'string' 형식은 'number' 형식에 할당할 수 없습니다.ts(2322)
자바스크립트에서는 오류가 나지 않겠지만 타입스크립트에서는 a라는 변수가 선언될 때 할당되는 데이터의 자료형태를 보고 number 타입을 추론하였다.
타입을 따로 지정해주지 않았지만 a는 number 타입으로 추론된 상태에서 string 타입을 저장하게 되면 오류를 발생시킨다.
함수의 반환값에 대한 타입 추론도 가능하다.
함수의 매개 변수의 타입이 지정되었거나 초기값이 설정되어있다면 타입스크립트는 매개 변수의 타입을 추론할 수 있다.
매개 변수의 타입이 number로 지정되어있고 타입스크립트가 매개변수 타입을 추론한 상태에서 함수의 반환 값이 n1 + n2는 당연히 타입스크립트에서 반환 값이 number 타입을 추론할 수 있다.
위 예제도 n1은 number 타입을 추론하였고 n3도 변수의 추론 방법에 의해 string 타입으로 추론 되었다.
그 후 return 값이 n1 + n3 이는 곧 정수형과 문자열의 + 연산을 의미하며 자바스크립트에서는 숫자가 문자열로 변환되어 문자열이 반환된다. 타입스크립트에서도 n1, n3의 타입을 알고있고 연산을 수행한 반환 값이 string 타입이란 것을 추론할 수 있다.
많은 글을 보면 타입 추론이 가능하면 타입을 지정해주지 않는 것이 좋다. 유연하게 컴파일러가 타입을 아는 경우에는 생략해주자 굳이 불필요한 작업이고 코드가 길어지는 작업이다.
나 역시 이렇게 알고 타입 추론이 가능한 부분은 타입을 명시해주지 않았다. 근데 팀원 중 한 분이 나는 타입을 다 명시해주는게 코드를 읽기 더 편하고 가독성도 좋아지는 것 같다고 하셧다.
생각해보니 충분히 그럴수도 있다. 나 역시 java를 사용하면서 모든 타입을 자연스럽게 지정하면서 사용했었던 것이다. 당시 불편함도 없었고 지금 생각해보면 굉장히 체계적이고 탄탄한 언어라고 생각한다.
결국 개발이라는 영역에는 정답은 없는 것 같다. 동작은 같은데 누구는 이게 마음에 들고 누구는 이게 마음에 들고 누가 정답이라고 할 수 있겠는가? 상황에 따라 더 효율적인 방법을 선택하는 것이 좋지 않을까 싶다.
타입 가드는 특정 변수가 특정 타입인지를 확인하며 특정 코드 블록 내에서 변수의 타입을 확실하게 보장하는 기능이다.
쉽게 생각하면 개발할 때 if문으로 분기처리와 비슷하다고 생각하면 된다.
Math.abs는 숫자 절대값을 구해는 메서드다. 하지만 반드시 number 타입의 데이터를 인자로 넘겨줘야 오류가 없다.
getMax 함수에서 타입스크립트는 매개변수로 받은 number가 number 타입일지 string 타입일지 확신이 없다는 것이다. 반드시 number 타입일 때만 해당 로직을 수행할 수 있으니깐.
이때 사용하는 것이 타입 가드다.
function getMax1(number: number | string) {
if (typeof number === 'string') {
return;
}
const max = Math.abs(number);
return max;
}
function getMax2(number: number | string) {
if (typeof number === 'number') {
const max = Math.abs(number);
return max;
}
}
음 조건 처리만 잘해주면 타입 가드가 되는 것 같다. 타입 가드가 반드시 어떠한 형태를 나타내는 것이 아닌 상황에 따라 더 가독성 좋게 효율적이게 가드를 하는 것이 좋아 보인다.
요약하면 해당 타입일때만 수행하는 로직이 있다면 타입 가드를 통해 원하는 타입을 보장하여 로직을 안전하게 수행할 수 있다.
typeof 외 타입 가드 방법에는 instancof, isArray, in, keyof, 옵셔널 체이닝 등이 있다.
타입 단언은 개발자가 특정 값의 타입을 확실하게 알고 있을 때 사용한다.
타입스크립트가 타입을 추론하는 것이 아닌 개발자가 직접 해당 타입이 맞다고 정의한다고 생각하면 된다.
function getMax(number: number | string) {
const max = Math.abs(number as number);
return max;
}
매개변수의 number는 number 타입일 때 Math.abs 메서드를 사용할 수 있다. 원래라면 에러를 발생해야하는 as 키워드로 number 변수가 반드시 number 타입이라고 단언하는 경우다.
as를 통해 number 타입을 단언하면 에러 표시는 사라지지만 문제가 있다 만약 string 타입일 때는 어떻게 되는가??
as 단언은 컴파일 단계에서는 에러를 발생해주지 않는다. 컴파일 단계에서는 number 타입을 확정하고 있기 때문이다.
런타임 단계에서는 string 타입의 변수가 들어오게 되면 오류를 발생한다. 즉 as 키워드는 컴파일 단계에서 타입을 단언하며 에러를 발생시키지 않고 런타임 단계가 되어서야 잘못된 타입을 감지할 수 있다.
이런 이유로 as를 남발해서는 안좋다. 확실한 타입이라고 생각되는 부분에 as를 편하게 사용하는 것이 좋다. 하지만 웬만해서 타입가드로 해결할 수 있다면 타입가드를 활용하자.
타입 호환은 특정 타입의 값을 다른 타입의 값으로 간주할 수 있는지를 결정한다. 특정 타입이 다른 타입과 호환될 수 있는지를 그 구조와 관계를 통해 판단한다.
함수의 타입 호환성을 결정하는 두 가지 기본 원칙이 있다. 매개변수의 수와 타입, 그리고 반환 타입이다.
let x = (a: number) => 0;
let y = (b: number, s: string) => 0;
y = x; // OK
x = y; // Error
첫 번째 할당 y = x;는 가능하다. 더 많은 매개변수를 가진 함수 타입이 더 적은 매개변수를 가진 함수 타입에 할당될 수 있다는 원칙 때문이다.
두 번째 할당 x = y;는 불가능하다. 이는 y가 두 개의 매개변수를 요구하는데 반해, x는 하나의 매개변수만을 가지고 있기 때문이다. x는 y의 모든 매개변수를 처리할 수 없으므로 타입 에러가 발생한다.
let x = () => ({name: "Alice"});
let y = () => ({name: "Alice", location: "Seoul"});
x = y; // OK
y = x; // Error
첫 번째 할당 x = y;는 가능하다. y의 반환 타입은 x의 반환 타입의 서브타입이다. 즉, y가 반환하는 객체는 x가 반환하는 객체가 가진 모든 속성을 포함하고 있다. 따라서 y를 x에 할당하는 것은 가능하다.
하지만 두 번째 할당 y = x;는 불가능하다. x가 반환하는 객체는 y가 반환하는 객체의 모든 속성을 가지고 있지 않다. x는 location 속성이 없기 때문에, x를 y에 할당하려고 하면 타입 에러가 발생한다.
결론적으로, 함수의 타입 호환성이 함수의 매개변수 수와 타입, 반환 타입에 따라 결정된다는 것이다.
interface Empty<T> {
}
let x: Empty<number>;
let y: Empty<string>;
x = y; // OK, y도 Empty로, T는 아무런 영향을 주지 않습니다.
interface NotEmpty<T> {
data: T;
}
let z: NotEmpty<number>;
let t: NotEmpty<string>;
z = t; // Error, number는 string과 호환되지 않습니다.
제너릭의 호환성은 제너릭 타입의 구조와 그 제너릭이 어떻게 사용되는지에 따라 결정된다.
제너릭 타입의 실제 타입이 사용되지 않으면 두 제너릭 타입은 호환되지만, 제너릭 타입의 실제 타입이 사용된다면 두 제너릭 타입의 호환성은 그 실제 타입의 호환성에 따라 결정된다.
interface Named1 {
name: string;
}
let x1: Named1;
let y1 = { name: "Alice", location: "Seoul" };
x1 = y1; // OK
x1는 name 속성을 요구하고 y1은 name과 location 속성을 가진 상태에서 x1에 대입할려고 한다.
이건 가능하다. 꼭 필요한 name 속성은 있고 location 속성은 불필요하지만 꼭 필요한 name 속성을 가지고 있기 때문에 호환된다.
interface Named2 {
name: string;
location: string;
}
let x2: Named2;
let y2 = { name: "Alice" };
x2 = y2; // Error
x2는 name, loaction 속성을 반드시 요구하고 이쏙 y2는 naem 속성만을 가진채 x2에 대입할려고 한다.
이건 불가능하다. x2는 name, loaction 속성이 필수인데 y2는 name 속성은 있지만 location 속성이 없기 때문이다.
이처럼 구조적으로 유사한 정도를 비교하여 호환이 가능한지 판별한다. 호환을 활용한다면 불필요한 타입 선언과 지정을 줄일 수 있다.
enum Test {
Ready,
Waiting,
}
let temp: Test = Test.Ready;
let temp2 = 0;
temp = temp2; // OK
enum은 기본적으로 타입을 지정하지 않으면 number 타입이 지정된다. 이 number 타입의 enum은 다른 number 타입과 호환된다.
enum Color {
Red,
Green,
Blue,
}
enum Shape {
Circle,
Square,
Triangle,
}
let colorValue: Color = Color.Red;
let shapeValue: Shape = Shape.Circle;
colorValue = shapeValue; // 오류! 이넘 타입 간 호환 불가능
enum과 enum이 같은 타입이라도 호환이 불가능하다. enum의 타입은 고유하다. 그래서 한 enum끼리는 같은 타입의 데이터여도 enum 자체가 서로 다른 고유한 타입이기에 호환되지 않는다.