💻 Frontend

TypeScript 계층

category
💻 Frontend
위의 블로그를 번역한 글입니다. 자세한 글은 위 블로그를 참고해주세요.

간단한 타입스크립트 예제

다음 코드를 읽고 머릿속에서 각 할당에 대한 오류가 있는지 예측해보세요.
  • any and unknown
let stringVariable: string = 'string' let anyVariable: any let unknownVariable: unknown anyVariable = stringVariable unknownVariable = stringVariable stringVariable = anyVariable stringVariable = unknownVariable
stringVariable = anyVariable string type에 any 값을 대입해서 오류가 생길 것 같다.
stringVariable = unknownVariable string type에 unknown 값을 대입해서 오류가 생길 것 같다.
결과는?
let stringVariable: string = "string"; let anyVariable: any; let unknownVariable: unknown; anyVariable = stringVariable; unknownVariable = stringVariable; stringVariable = anyVariable; stringVariable = unknownVariable; // 'unknown' 형식은 'string' 형식에 할당할 수 없습니다.
  • never
let stringVariable: string = 'string' let anyVariable: any let neverVariable: never neverVariable = stringVariable neverVariable = anyVariable anyVariable = neverVariable stringVariable = neverVariable
stringVariable = neverVariable string type에 never 값을 대입해서 오류가 생길 것 같다.
anyVariable = neverVariable any type에 never type은 호환이 되지 않을 것 같아 오류가 생길 것 같다.
결과는?
let stringVariable: string = "string"; let anyVariable: any; let neverVariable: never; neverVariable = stringVariable; // 'string' 형식은 'never' 형식에 할당할 수 없습니다. neverVariable = anyVariable; // 'any' 형식은 'never' 형식에 할당할 수 없습니다. anyVariable = neverVariable; stringVariable = neverVariable;
  • void
let undefinedVariable: undefined let voidVariable: void let unknownVariable: unknown voidVariable = undefinedVariable undefinedVariable = voidVariable voidVariable = unknownVariable
voidVariable = undefinedVariable void type에 undefined type은 호환이 되지 않을 것 같아 오류가 생길 것 같다.
function fn(cb: () => void): void { return cb() } fn(() => 'string')
fn(() => 'string') void를 반환해줘야 하는데 string을 반한해줘서 오류가 생길 것 같다.
결과는?
let undefinedVariable: undefined; let voidVariable: void; let unknownVariable: unknown; voidVariable = undefinedVariable; undefinedVariable = voidVariable; // 'void' 형식은 'undefined' 형식에 할당할 수 없습니다. voidVariable = unknownVariable; // 'unknown' 형식은 'void' 형식에 할당할 수 없습니다.
function fn(cb: () => void): void { return cb() } fn(() => 'string') -> 오류 X
 

It is a hierarchy tree.

TypeScript의 모든 타입이 계층 구조에서 자리를 차지합니다. tree와 같은 구조로 시각화 할 수 있습니다. 트리에서는 최소한 부모 노드와 자식 노드가 있습니다. 이러한 관계에 대해 상위 노드(부모 노드)를 상위 타입이라 하고 하위 노드(자식 노드)를 하위 타입이라 합니다.
notion image
 
객체 지향 프로그래밍 개념 중 하나인 상속에 익숙할 것입니다. 상속은 자식 클래스와 부모 클래스 사이에 is-a 관계를 설정합니다. 부모 클래스가 Vehicle이고 자식 클래스가 Car인 경우 관계는 “Car is Vehicle” 입니다. 그러나 다른 방식으로는 작동하지 않습니다. 하위 클래스의 인스턴스는 논리적으로 상위 클래스의 인스턴스가 아닙니다. “Vehicle is not Car” 이것이 상속의 의미론적 의미이며 TypeScript의 타입 계층 구조에도 적용됩니다.
리스코프 치환 원칙에 따르면 Vehicle(상위 타입)의 인스턴스는 프로그램의 정확성을 변경하지 않고 자식 클래스(하위 타입) Cars의 인스턴스로 대체 가능해야 합니다. 즉, 타입(Vehicle)에서 특정 동작을 기대하는 경우 해당 하위 타입(Car)은 이를 존중해야 합니다.
이를 종합하면 TypeScript에서 타입의 하위 타입 인스턴스를 해당 상위 타입의 인스턴스에 할당/대체할 수 있지만 그 반대는 아닙니다. (상위 타입에 하위 타입 할당 가능, 그러나 하위 타입에 상위 타입 할당 불가능.)
 

nominal and structural typing

상위 타입/하위 타입 관계가 적용되는 두 가지 방법이 있습니다. 첫번째는 대부분 정적 타입 언어(ex: Java)에서 사용하는 명목적 타이핑이라고 합니다. 여기서 타입을 명시적으로 선언해야 하는 경우 class Foo extends Bar 와 같은 구문을 통해 표현할 수 있다. 두번째는 TypeScript에서 사용하는 구조적 타이핑으로, 코드에서 명시적으로 관계를 표시할 필요가 없습니다. Foo 타입의 인스턴스는 Foo에 일부 추가 구성원이 있더라고 Bar타입의 하위 타입입니다.
상위 타입-하위타입 관계에 대해 생각하는 또 다른 방법은 어떤 타입이 더 엄격한지 확인하는 것입니다. {name:string, age:number} 타입이 {name:string} 타입보다 더 엄격합니다. 따라서 {name:string, age:number} 타입은 {name:string} 타입의 하위 타입입니다.
notion image
 
 

two ways of checking assignability/substitutability

TypeScript의 타입 계층 트리로 들어가기 전에 마지막으로 한 가지:
  • 타입 캐스트: 어떠한 타입의 변수를 다른 타입의 변수에 할당하여 타입 오류가 발생하는지 확인할 수 있습니다.
  • extends 키워드 - 타입을 다른 타입과 확장하기
type A = string extends unknown? true : false; // true type B = unknown extends string? true : false; // false
 

the top of the tree

TypeScript에는 다른 모든 타입의 상위 타입인 any와 unknown이 있습니다. 다른 모든 타입을 포함하여 모든 타입의 값을 허용합니다.
notion image
 
 

upcast & downcast

타입 캐스트에는 업캐스트와 다운캐스트의 두 가지 유형이 있습니다.
notion image
 
업캐스트는 상위 타입하위 타입을 지정하는 것, 트리를 올라가는 것과 비슷하다고 생각할 수 있습니다. 더 엄격한 하위 타입을 더 일반적인 상위 타입으로 대체하는 것입니다. (상위에 하위를 지정)
예를 들어 모든 문자열 타입은 any 타입과 unknown 타입의 하위 타입입니다. 즉, 다음과 같은 할당이 허용됩니다.
let stringVal: string = 'foo' let anyVal: any = string // ✅ ⬆️upcast let unknownVal: unknown = string // ✅ ⬆️upcast
그 반대는 다운캐스트라고 합니다. 보다 일반적인 상위 타입을 보다 엄격한 하위 타입으로 대체하여 트리를 따라 내려가는 것으로 생각하면 된다. (하위에 상위를 지정)
업캐스트와 달리 다운캐스트는 안전하지 않으며 대부분의 강력한 형식의 언어에서는 이를 자동으로 허용하지 않습니다. 예를 들어 문자열 타입에 anyunknown 타입의 변수를 할당하는 것은 다운캐스트입니다.
let anyVal: any let unknownVal: unknown let stringA: string = any // ✅ ⬇️downcast - it is allowed because `any` is different.. let stringB: string = unknown // ❌ ⬇️downcast
내가 업캐스트와 다운캐스트의 정의를 몰라 아까 위의 문제들을 틀렸던 것 같다.
정리를 하자면 업캐스트는 상위 타입에 하위 타입을 지정하는 것이다. 타입스크립트 상에서 에러가 나지 않는다. 다운캐스트는 하위 타입에 상위 타입을 지정하는 것이다. 타입스크립트 상에서는 any로 지정해준 것 이외에는 에러가 발생한다.
 
// 상위 타입 interface Animal { name: string; kind: string; } // Animal을 상속 받았기 떄문에 하위 타입. interface Dog extends Animal { bowow: "bowow"; } let animal: Animal = { name: "asd", kind: "animal", }; let dog: Dog = { name: "asd", kind: "animal", bowow: "bowow", }; dog = animal; // downcast animal = dog; // upcast
Dog 인터페이스에는 bowow라는 속성이 선언되어 있는데 animal이라는 값을 넣게 된다면 당연히 에러가 발생할 것이다. 이것이 다운캐스트이다.
 

the bottom of the tree

never 타입은 더 이상의 확장이 되지 않는 트리의 맨 아래 입니다. 한마디로 최하위 타입인 것이죠.
notion image
 
대칭적으로 any 및 unknown은 모든 값을 허용하는 반면, never 타입은 상위 타입의 안티 타입으로 작동하기 때문에 모든 타입의 하위 타입이므로 모든 값을 허용하지 않습니다.
let anyVal: any let numberVal: number = 5 let neverVal: never = any // ❌ ⬇️downcast neverVal = number // ❌ ⬇️downcast numberVal = never // ✅ ⬆️upcast
당신이 충분히 열심히 생각했다면, 리스코프 치환 원칙에 따라 TypeScript의 타입 시스템에 있는 다른 모든 타입, 즉 상위 타입에 할당하거나 대체할 수 있어야 하므로 무한한 양의 타입과 멤버를 가져서는 안된다는 것을 깨달았을 것입니다. 예를 들어, 우리 프로그램은 numberstringnever로 대체한 후에 올바르게 작동해야 합니다. never는 문자열과 숫자 타입의 하위 타입이고 상위 타입에 의해 정의된 동작을 중단해서는 안 되기 때문입니다.
 

마무리

타입에는 계층이 존재한다!
최상위 타입은 unknown, 최하위 타입은 never이다.
상위타입에서 하위타입으로 갈수록 더 엄격해집니다.
any 및 unknown 타입은 모든 타입의 값을 대체할 수 있고, never는 모든 값을 허용할 수 없습니다.
업캐스트는 상위 타입 변수에 하위 타입 변수를 지정한 것이고 다운캐스트는 하위 타입 변수에 상위 타입 변수를 지정한 것이다.