타입 호환성 (Type Compatibility)

https://objectcomputing.com/resources/publications/sett/typescript-the-good-partshttps://www.typescriptlang.org/ko/docs/handbook/type-compatibility.html
1. 타입 호환성이란?
타입 호환성은 어떤 타입의 값을 다른 타입으로 취급해도 괜찮은지 판단하는 것을 말한다.
TypeScript에서는 타입 간의 관계에 따라 값을 할당할 수 있는 경우와 할당할 수 없는 경우가 있다.
예를 들어 리터럴 타입은 더 넓은 원시 타입에 할당할 수 있다.
let literal: 'hello' = 'hello';
let str: string = literal; // 가능'hello'는 문자열 중 하나의 값이므로, 더 넓은 타입인 string에 할당할 수 있다.
2. 슈퍼타입과 서브타입

타입 호환성을 이해하려면 슈퍼타입(Supertype)과 서브타입(Subtype)을 알아두면 좋다.
슈퍼타입(Supertype): 더 넓은 범위의 값을 포함하는 타입서브타입(Subtype): 더 좁은 범위의 값을 포함하는 타입
let literal: 'hello' = 'hello';
let str: string = literal; // 가능'hello'는 string에 포함되는 하나의 값이다.
따라서 'hello' 리터럴 타입은 string 타입의 서브타입이고, string은 'hello'의 슈퍼타입이다.
반대로 더 넓은 타입을 더 좁은 타입에 할당하는 것은 안전하지 않을 수 있다.
let str: string = 'hello';
let literal: 'hello' = str; // Errorstr의 현재 값은 'hello'지만, 타입상으로는 어떤 문자열이든 될 수 있다.
따라서 'hello'만 허용하는 타입에 바로 할당할 수 없다.
3. 업캐스팅 (Upcasting)
업캐스팅은 서브타입을 슈퍼타입으로 취급하는 것이다.
즉, 더 구체적인 타입을 더 넓은 타입으로 할당하는 것을 말한다.
let num: 10 = 10;
let numberValue: number = num; // 가능10은 number에 포함되는 값이므로, 10 타입은 number 타입의 서브타입이다.
따라서 더 넓은 타입인 number에 할당할 수 있다.
let strLiteral: 'hello' = 'hello';
let stringValue: string = strLiteral; // 가능'hello'도 string에 포함되는 값이므로, 더 넓은 타입인 string에 할당할 수 있다.
4. 다운캐스팅 (Downcasting)
다운캐스팅은 슈퍼타입을 서브타입으로 취급하는 것이다.
즉, 더 넓은 타입을 더 구체적인 타입으로 할당하는 것을 말한다.
다운캐스팅은 안전하지 않을 수 있기 때문에 TypeScript는 기본적으로 허용하지 않는다.
let numberValue: number = 10;
let num: 10 = numberValue; // ErrornumberValue는 현재 값이 10이더라도, 타입상으로는 어떤 숫자든 될 수 있다.
따라서 10만 허용하는 리터럴 타입에 바로 할당할 수 없다.
let stringValue: string = 'hello';
let strLiteral: 'hello' = stringValue; // ErrorstringValue는 현재 값이 'hello'일 수도 있지만, 다른 문자열일 수도 있다.
그래서 'hello'만 허용하는 타입에 할당할 수 없다.
4-1. 타입 단언으로 다운캐스팅하기
다운캐스팅이 필요한 경우 as를 사용해 타입을 단언할 수 있다.
이를 타입 단언(Type Assertion)이라고 한다.
let stringValue: string = 'hello';
let strLiteral = stringValue as 'hello';하지만 타입 단언은 TypeScript에게 “이 값의 타입을 내가 더 잘 알고 있다”고 알려주는 방식이다.
잘못 사용하면 실제 값과 타입이 달라질 수 있으므로 주의해야 한다.
let value: string = 'world';
let hello = value as 'hello'; // 컴파일은 가능하지만 실제 값은 'world'5. 타입 계층에서의 특수 타입
TypeScript에는 타입 계층에서 특별한 위치를 가지는 타입들이 있다.
대표적으로 unknown, never, void, any가 있다.
5-1. unknown
unknown 타입은 모든 타입의 슈퍼타입이다.
즉, 대부분의 타입을 unknown에 할당할 수 있다.
let value1: unknown = 1; // number -> unknown
let value2: unknown = 'hello'; // string -> unknown
let value3: unknown = true; // boolean -> unknown
let value4: unknown = null; // null -> unknown
let value5: unknown = undefined; // undefined -> unknown
let value6: unknown = []; // array -> unknown
let value7: unknown = {}; // object -> unknown
let value8: unknown = () => {}; // function -> unknown하지만 unknown 타입의 값은 다른 타입에 바로 할당할 수 없다.
let unknownValue: unknown = 'hello';
let str: string = unknownValue; // Error
let num: number = unknownValue; // Errorunknown은 어떤 값이든 받을 수 있지만, 사용할 때는 타입을 먼저 확인해야 한다.
let unknownValue: unknown = 'hello';
if (typeof unknownValue === 'string') {
let str: string = unknownValue; // 가능
}5-2. never
never 타입은 모든 타입의 서브타입이다. 즉, never 타입의 값은 다른 타입에 할당할 수 있다.
다만 never는 실제로 존재할 수 없는 값을 의미하기 때문에, 일반적으로 직접 값을 만들 수 없다.
function throwError(): never {
throw new Error('error');
}never 타입은 모든 타입에 할당할 수 있다.
declare const neverValue: never;
let num: number = neverValue; // 가능
let str: string = neverValue; // 가능
let bool: boolean = neverValue; // 가능
let nullValue: null = neverValue; // 가능
let undefinedValue: undefined = neverValue; // 가능
let arr: string[] = neverValue; // 가능
let obj: object = neverValue; // 가능반대로 다른 타입의 값을 never에 할당할 수는 없다.
let neverValue: never;
neverValue = 1; // Error
neverValue = 'hello'; // Error
neverValue = true; // Error
neverValue = null; // Error
neverValue = undefined; // Error
neverValue = []; // Error
neverValue = {}; // Errornever는 “아무 값도 가질 수 없는 타입”에 가깝기 때문에, 어떤 일반 값도 never 타입이 될 수 없다.
5-3. void
void는 주로 함수가 반환값을 사용하지 않을 때 사용하는 타입이다.
타입 계층상 void는 undefined의 슈퍼타입이다.
따라서 undefined는 void에 할당할 수 있다.
function noReturnFunc(): void {
console.log('hi');
}
function returnUndefined(): void {
return undefined;
}
function returnOnly(): void {
return;
}
function emptyFunc(): void {}JavaScript에서 함수가 아무 값도 반환하지 않으면 기본적으로 undefined가 반환된다.
따라서 반환 타입이 void인 함수에서 return undefined를 해도 오류가 발생하지 않는다.
function logMessage(): void {
console.log('hello');
}
const result = logMessage();
console.log(result); // undefined하지만 void는 “반드시 undefined를 반환해야 한다”는 뜻이 아니다.void는 반환값을 기대하지 않는 함수의 타입을 표현할 때 사용한다.
5-4. any
any는 타입 검사를 사실상 우회하는 타입이다.
타입 계층 안에서 안전하게 동작한다기보다는, TypeScript의 타입 검사를 끄는 것에 가깝다.
let anyValue: any;
let num: number = anyValue; // 가능
let str: string = anyValue; // 가능
let bool: boolean = anyValue; // 가능
anyValue = num; // 가능
anyValue = str; // 가능
anyValue = bool; // 가능any는 어떤 타입에도 할당할 수 있고, 어떤 타입의 값도 받을 수 있다.
그래서 편리하지만 TypeScript가 오류를 잡아주지 못할 수 있다.
let anyValue: any = 'hello';
anyValue.toUpperCase(); // 가능
anyValue = 123;
anyValue.toUpperCase(); // 컴파일 에러는 없지만 런타임 에러 발생단, never는 예외적으로 주의해야 한다.never는 아무 값도 가질 수 없는 타입이기 때문에, any도 never에 할당할 수 없다.
let anyValue: any;
let neverValue: never = anyValue; // Error