📗

4장 객체 타입

4.1 타입스크립트의 객체 타입

타입스크립트에서 객체 타입은 특정 프로퍼티와 메서드를 가지는 객체의 구조를 정의하는 데 사용됩니다. 타입스크립트는 정적 타입 언어이므로 객체의 타입을 명시적으로 선언할 수 있고, 그 외에도 다양한 방식으로 객체 타입을 정의할 수 있습니다.

4.1.1 객체 리터럴 선언

객체 리터럴은 중괄호 {}를 사용하여 객체를 직접 선언하는 방식입니다. 이 방식은 주로 간단한 객체를 생성하거나 인라인으로 타입을 지정할 때 사용됩니다.
let newcomer = { name: "Tommy", age: 30, position: "Frontend" };
위의 코드에서 신입사원에 대한 정보가 담긴 newcomer는 객체 리터럴 선언을 사용하여 생성된 객체입니다. 타입스크립트는 이 객체의 프로퍼티를 분석하여 해당 변수의 타입을 추론합니다. 이 방식은 간편하지만, 타입스크립트가 타입을 추론할 수 있는 경우에만 유용합니다.

4.1.2 명시적 선언

명시적 선언은 타입을 직접 명시하여 객체를 선언하는 방식입니다. 주로 복잡한 객체나 특정한 타입을 가진 객체를 생성할 때 사용됩니다.
interface Developer { name: string; age: number; position: string; } let newcomer: Developer = { name: "Tommy", age: 30, position: "Frontend" };
위의 코드에서 Developer 인터페이스를 사용하여 신입사원의 정보가 담긴 newcomer이라는 객체를 명시적으로 선언했습니다. 이 방식은 객체의 구조를 명확하게 정의하고 타입을 강제할 수 있어 유용합니다. 또한 명시적 선언은 코드의 가독성을 높이고 잠재적인 오류를 감소시키는 데 도움이 됩니다.
이처럼 객체 리터럴 선언 방식은 코드를 간결하게 작성하는 데는 적합하지만, 코드의 가독성과 유지 보수를 위해서는 명시적 선언으로 객체 타입을 정의하는 것이 좋습니다. 두 방식은 상황에 따라 선택되고 사용되며, 코드의 명확성과 유연성을 고려하여 적절한 방법을 선택하는 것이 중요합니다.
 

4.2 타입 별칭으로 객체 타입 정의

타입 별칭(Type Alias)은 사용자가 새로운 타입을 정의할 수 있게 해주는 기능입니다. 주로 코드의 재사용과 유지 보수를 위해 사용하며, 객체 타입 정의에서 타입 별칭은 그 정의를 간단하게 만들어 줄 수 있습니다. 사용자가 정의한 것을 타입으로 사용할 수 있다는 점에서 아래에서 설명할 인터페이스와 유사합니다. 하지만 타입 별칭의 경우 인터페이스와 달리 객체, 원시 타입, 유니온 타입 등을 포함한 모든 타입을 선언할 수 있습니다.

4.2.1 기본 정의 방법

타입 별칭은 type이라는 키워드를 사용하여 정의합니다.
type MyString = string; type MyNumber = number; type MyUnion = string | null; type MyObj = {a: 1} | {b: 2}; type MyTuple = [string, boolean];
기존의 stringMyString이라는 이름을 붙이는 것과 같이, 타입 별칭은 새로 타입을 정의하여 만들어 내는 것이 아닌 기존의 타입에 새로운 이름을 지정하여 사용하는 방식입니다.
타입 별칭으로 객체 타입을 정의하는 방법은 다음과 같습니다.
type Developer = { name: string; age: number; job: string; };
위의 예제에서 Developer는 객체 타입으로서 name, age, job 프로퍼티를 가진 객체를 나타냅니다. 이제 이 타입 별칭을 사용하여 변수를 선언하거나 함수 매개변수 및 반환 타입으로 활용할 수 있습니다.
let myInfo: Developer = { name: "Wade", age: 24, job: "Developer" }; function printDeveloper(developer: Developer): void { console.log(developer); }
위의 코드에서는 Developer라는 타입 별칭을 사용하여 myInfo라는 객체를 선언하고 객체 내 프로퍼티 name, age, job에 각각의 값을 할당했습니다. 함수 printDeveloper에서도 타입 별칭으로 선언한 Developer를 사용하여 이 타입의 객체를 매개 변수로 받는다는 것을 알 수 있습니다.

4.2.2 교차 타입

때로는 이미 존재하는 타입을 조합하여 새로운 타입을 만들어야 할 때가 있습니다. 이런 경우에는 교차 타입(intersection types)을 사용할 수 있습니다. 교차 타입은 두 개 이상의 타입을 결합하여 하나의 타입으로 만드는 방법입니다.
type Developer = { name: string; age: number; job: string; }; type BackendDeveloper = { backendSkills: string[]; }; type FrontendDeveloper = { frontendSkills: string[]; }; type FullStackDeveloper = Developer & BackendDeveloper & FrontendDeveloper; let myInfo: FullStackDeveloper = { name: "Wade", age: 24, job: "Full Stack Developer", backendSkills: ["Node.js", "Express"], frontendSkills: ["HTML", "CSS", "JavaScript"] };
타입 별칭으로 정의해 준 타입들을 &(인터섹션) 연산자로 교차하여 교차 타입을 정의할 수 있습니다. 위 코드에서 FullStackDeveloperDevelopername, age, job 프로퍼티를, BackendDeveloperbackendSkills 프로퍼티를, FrontendDeveloperfrontendSkills 프로퍼티를 모두 가진 객체를 나타냅니다. 이로써 FullStackDeveloper를 정의하는 데 코드의 재사용성을 높일 수 있으며 중복된 타입 정의를 방지할 수 있습니다.
 

4.3 인터페이스로 객체 타입 정의

인터페이스는 주로 타입 체크를 위해 사용되며 변수, 함수, 클래스에 사용할 수 있습니다. 이는 새로운 타입을 정의하기 위해 여러 가지 타입을 갖는 프로퍼티로 이루어진 구조를 설계하는 데 사용됩니다. 즉 인터페이스에 선언된 프로퍼티 또는 메서드의 구현을 강제하여 일관성을 유지할 수 있도록 하는 것입니다.
인터페이스는 프로퍼티와 메서드를 가질 수 있다는 점에서 클래스와 유사합니다. 그러나 클래스와의 주요 차이점은 직접적으로 인스턴스를 생성할 수 없으며, 모든 메서드가 추상 메서드로 간주됩니다. 이는 해당 인터페이스를 구현하는 클래스에서 모든 메서드가 반드시 구현되어야 함을 의미합니다. 타입스크립트는 ES6와 달리 인터페이스를 지원하여 특정 구조나 동작을 강제하고 타입을 명시적으로 정의할 수 있습니다. 아래에서 간단하게 인터페이스의 역할에 대해 소개하겠습니다.
 
타입 체크 및 명시적 타입 정의
코드에서 변수, 함수, 클래스 등을 선언할 때 인터페이스를 사용하여 해당 구성 요소의 타입을 명시적으로 정의하고, 코드에서 사용되는 값이나 객체가 기대한 구조를 갖추고 있는지를 체크합니다.
interface Student { name: string; studentID: number; } function attendanceCheck(student: Student): string { return `${person.name} attended.`; } const jason: Student = { name: "Jason", studentId: 1841021 }; attendanceCheck(jason); // 정상 동작
 
구조의 일관성 유지
인터페이스는 여러 요소의 구조를 일관되게 정의하여 코드의 가독성을 향상시키고, 잘못된 사용이나 구현을 방지합니다.
interface Shape { color: string; } interface Square extends Shape { height: number; } const square: Square = { color: "red", height: 10 };
 
추상화 및 확장
인터페이스를 통해 코드를 추상화하고, 한 인터페이스를 다른 인터페이스에 확장하여 코드를 확장할 수 있습니다.
interface Vehicle { start(): void; stop(): void; } interface ElectricVehicle extends Vehicle { charge(): void; } class ElectricCar implements ElectricVehicle { start() { // ... } stop() { // ... } charge() { // ... } }
이처럼 타입스크립트의 인터페이스 역할을 통해 인터페이스가 코드에서 타입을 정의하고, 해당 타입을 사용하는 변수, 함수, 클래스 등의 구조를 강제함으로써 코드의 일관성과 안정성을 향상시킨다는 것을 알 수 있습니다. 아래에서는 인터페이스를 통해 객체 타입을 정의하는 방법을 살펴보겠습니다.

4.3.1 기본 정의 방법

interface Developer = { name: string; age: number; job: string; };
타입 별칭과 유사한 방법으로 인터페이스를 통한 객체 타입 선언이 가능합니다. type 키워드 대신 interface 키워드로 선언하면 정의할 수 있습니다. 인터페이스로 선언한 객체 타입을 사용하는 방식은 타입 별칭과 같습니다.
let myInfo: Developer = { name: "Wade", age: 24, job: "Developer" }; function printDeveloper(developer: Developer): void { console.log(developer); }
위의 코드에서 Developer라는 인터페이스를 정의하고, 이를 사용하여 myInfo라는 객체를 선언하고 함수의 매개 변수로 사용하고 있습니다. 이렇게 함으로써 코드를 더 읽기 쉽고 유지 보수하기 쉽게 만들 수 있습니다.

4.3.2 인터페이스 확장하기/합치기

인터페이스를 확장하거나 합치는 경우 extends 키워드를 사용합니다. 확장하면서 추가적인 프로퍼티나 메서드를 정의할 수 있으며, 두 개 이상의 인터페이스를 합치는 경우에는 extends 키워드 뒤에 합칠 인터페이스들을 나열합니다.
interface Developer { name: string; age: number; job: string; } interface FrontendDeveloper extends Developer { frontendSkills: string[]; } interface BackendDeveloper extends Developer { backendSkills: string[]; } interface FullStackDeveloper extends FrontendDeveloper, BackendDeveloper { // FullStackDeveloper를 위한 추가적인 프로퍼티나 메서드 정의 가능 } let myInfo: FullStackDeveloper = { name: "Wade", age: 24, job: "Full Stack Developer", frontendSkills: ["HTML", "CSS", "JavaScript"], backendSkills: ["Node.js", "Express"] };
Developer 인터페이스는 개발자의 기본 정보를 정의합니다. FrontendDeveloper 인터페이스는 Developer를 확장하여 Frontend 개발자의 정보와 추가적인 Frontend 스킬을 정의합니다.BackendDeveloper 인터페이스는 마찬가지로 Developer를 확장하여 Backend 개발자의 정보와 추가적인 Backend 스킬을 정의합니다. FullStackDeveloper 인터페이스는 FrontendDeveloperBackendDeveloper를 확장하여 Full Stack 개발자의 정보를 통합적으로 정의합니다.
따라서 myInfo객체는 FullStackDeveloper의 타입을 가지며, Frontend와 Backend 스킬을 모두 갖추고 있습니다. 이를 통해 코드의 일관성을 유지하고 다양한 개발자 유형에 대한 정보를 효율적이고 명확하게 표현할 수 있습니다.
 

4.4 구조적 타이핑

구조적 타이핑(Structural Typing)은 타입스크립트 및 다른 몇몇 프로그래밍 언어에서 타입 시스템의 한 접근 방식을 나타냅니다. 이 접근 방식에서는 객체의 타입이 해당 객체의 구조(프로퍼티와 메서드의 형태)를 기반으로 결정됩니다. 즉, 두 객체가 유사한 구조를 가지면, 두 객체는 같은 타입으로 간주합니다. 이는 타입의 실제 이름에 의존하여 타입을 비교하는 명목적 타이핑(Nominal Typing)과는 대조되는 개념입니다. 먼저 명목적 타이핑에 대해 간략하게 설명하겠습니다.

4.4.1 명목적 타이핑

명목적 타이핑은 프로그래밍 언어의 타입 시스템 중 하나로, 타입을 명시적으로 선언하고, 그 타입이 실제로 정의된 이름이나 명칭에 의해 결정되는 방식을 의미합니다. 이는 객체나 변수의 타입을 비교할 때 해당 타입의 이름이 일치해야만 호환성이 인정되는 시스템으로 C++, Java 등의 언어에서 주로 사용하는 방식입니다.
type Pet { name: string; } type Dog { name: string; } let pet: Pet; pet = new Dog(); // Error: 명목적 타이핑에서는 타입의 이름이 일치해야 함
당연한 말이지만 두 객체나 클래스가 동일한 이름의 타입을 가지더라도 구조적으로 다르면 호환되지 않습니다.
interface Entity { name: string; } class Person implements Entity { constructor(public name: string, public age: number) {} } class Animal implements Entity { constructor(public name: string, public species: string) {} } function printEntity(entity: Entity) { console.log(`Entity Name: ${entity.name}`); } // Person 객체는 Entity 타입과 호환됩니다. const person: Person = new Person("John Doe", 25); printEntity(person); // Animal 객체도 Entity 타입과 호환됩니다. const animal: Animal = new Animal("Tiger", "Mammal"); printEntity(animal); // 하지만 Person과 Animal은 서로 호환되지 않습니다. // Error: 명목적 타이핑에서는 객체가 구조적으로 일치해야 함 // const entity1: Entity = new Person("Invalid Person", 30); // Error // const entity2: Entity = new Animal("Invalid Animal", "Unknown"); // Error
PersonAnimal이 각각 Entity를 명시적으로 상속받아 구현하고 있지만, 이들은 구조적으로 서로 다르기 때문에 호환되지 않는다는 점입니다.

4.4.2 구조적 타이핑

구조적 타이핑은 타입의 이름이 아닌 오직 객체의 멤버만으로 타입을 관계시키는 방식입니다. 즉, 두 객체가 타입의 이름이나 명시적으로 선언된 관계없이도 유사한 구조(같은 프로퍼티와 메서드)를 가지면, 두 객체는 같은 타입으로 간주됩니다. 구조적 타이핑의 관점으로 보면, 위의 명시적 타이핑에서 에러가 나던 코드가 정상적으로 동작합니다.
type Pet { name: string; } type Dog { name: string; } let pet: Pet; pet = new Dog(); // OK: 같은 구조의 타입을 가진 두 객체 = 구조적 타이핑
구조적 타이핑은 덕 타이핑이라는 개념에서 유래되었는데, 덕 타이핑에 대해 먼저 설명한 후 구조적 타이핑에 대해 더 자세히 알아보겠습니다.

1) 덕 타이핑

만약 어떤 새가 오리처럼 걷고, 헤엄치고, 꽥꽥거리는 소리를 낸다면 나는 그 새를 오리라고 부를 것이다.
위의 인용문은 덕 타이핑(Duck Typing)을 가장 잘 드러내는 한 문장이라고 볼 수 있으며, 객체의 실제 타입보다는 해당 객체가 가져야 하는 특정한 행동을 기준으로 타입을 판단한다는 뜻입니다. 즉, 덕 타이핑은 객체의 타입을 명시적으로 지정하는 것이 아니라 객체의 메서드나 프로퍼티가 적절한지에 따라 객체를 특정 타입에 속하는 것으로 간주합니다.
다음 코드는 덕 타이핑에 대한 위키백과의 예시 코드를 타입스크립트 코드로 변환한 것입니다. 클래스와 함수 단위로 코드를 살펴보겠습니다.
class Duck { quack(): void { console.log("꽥꽥!"); } feathers(): void { console.log("오리에게 흰색, 회색 깃털이 있습니다."); } } class Person { quack(): void { console.log("사람이 오리를 흉내 냅니다."); } feathers(): void { console.log("사람은 깃털은 없지만 털이 있습니다."); } } function inTheForest(duck: Duck): void { duck.quack(); duck.feathers(); } function game(): void { const donald: Duck = new Duck(); const john: Person = new Person(); inTheForest(donald); inTheForest(john); // Runtime Error: Person 타입은 Duck 타입과 호환되지 않음 }
Class Duck, Class Person : Duck 클래스와 Person 클래스는 각각 quack()과 feathers() 메서드를 가지고 있습니다. 이 메서드들은 덕 타이핑에서 중요한 역할을 합니다. 즉, 오리라는 객체는 quack() 메서드로 소리를 낼 수 있고, feathers() 메서드로 깃털의 색상을 확인할 수 있어야 합니다.
inTheForest() : inTheForest 함수는 Duck 타입의 인자를 받아서 해당 인자의 quack()과 feathers() 메서드를 호출합니다. 이 함수는 오리가 행하는 특정한 행동을 수행합니다.
game() : game 함수에서는 donald 객체를 Duck 타입으로 선언하고, john 객체를 Person 타입으로 선언합니다. 그다음, inTheForest 함수에 각 객체를 전달합니다. 이때 donald는 Duck 타입이므로 예상대로 작동하지만, john은 Person 타입이므로 Duck 타입과 호환되지 않습니다. 이것은 john은 오리가 아니므로 inTheForest 함수에 전달되지 않는 점에서 덕 타이핑의 핵심 원칙을 잘 보여주는 예시입니다.
Person 클래스에서 ‘사람이 오리를 흉내 내고 깃털이 없지만 털이 있다’는 출력을 하는 것으로도 오리를 흉내 내는 것이기 때문에 Duck 클래스와 호환되지 않을까?
왜 에러가 뜨는 것인지 설명이 추상적으로 와닿을 수 있습니다. 그렇다면 다시 덕 타이핑을 잘 드러내는 인용문을 보면 됩니다. 인용문의 관점에서 덕 타이핑은 객체의 행동에 중점을 두므로, john이 오리처럼 행동한다면(즉, quack()과 feathers() 메서드를 가지고 있다면) john을 Duck 타입으로 간주할 수 있습니다.
그러나 위의 코드에서는 Person 클래스가 quack() 메서드를 "사람이 오리를 흉내 냅니다."라는 메시지로 구현하고 있습니다. 이는 오리의 진짜 “꽥꽥!”거리는 소리가 아니므로 덕 타이핑의 관점에서는 quack() 메서드가 오리처럼 행동하는 것으로 인식되지 않습니다(feathers 함수도 이와 동일). 따라서 런타임 과정에서 inTheForest(john)에서는 타입스크립트 컴파일러가 타입 에러를 발생시킬 것입니다.
따라서 덕 타이핑의 관점에서 john이 Duck 객체로 간주되려면 quack() 메서드(그리고 feathers() 메서드)가 오리처럼 정확하게 동작해야 합니다. 이것이 덕 타이핑의 핵심 아이디어 중 하나인데, 객체가 특정한 행동을 수행할 수 있다면 해당 객체를 해당 타입으로 간주하는 것입니다.
만약 Person 클래스의 quack() 메서드가 오리와 똑같이 "꽥꽥!"이라고 출력한다면, 덕 타이핑의 관점에서는 Person 객체를 Duck 타입으로 취급할 수 있게 됩니다(feathers 함수도 이와 동일). 이렇게 코드를 수정한다면 inTheForest(john)에서 에러가 발생하지 않을 것입니다.

2) 구조적 타이핑

구조적 타이핑은 명시적인 선언이나 이름을 기반으로 하는 명목적 타이핑과도 다르고, 덕 타이핑을 기반으로 하지만 런타임에 타입을 체크하는 덕 타이핑과도 다릅니다. 명목적 타이핑과는 아예 상반되는 개념이기 때문에 덕 타이핑과의 어떤 차이가 있는지를 중점으로 설명해 보겠습니다.
타입 결정 기준
덕 타이핑은 객체의 행동, 즉 객체가 특정한 메서드나 프로퍼티를 가지고 있다면 해당 객체를 특정 타입으로 간주합니다. 반면 구조적 타이핑은 객체의 구조가 해당 타입의 요구 사항과 일치하는지를 확인합니다. 따라서 객체가 필요한 프로퍼티와 메서드를 가지고 있다면 해당 객체를 해당 타입으로 간주합니다. 이는 객체의 특정한 행동에 의존하지 않고, 단순히 프로퍼티와 메서드의 존재 여부로 타입을 결정합니다.
명시적 타입 인터페이스
덕 타이핑은 명시적인 타입 인터페이스를 구현할 필요가 없습니다. 객체가 필요한 행동을 충족시키면 해당 타입으로 간주합니다. 또한 구조적 타이핑도 마찬가지로 명시적인 인터페이스 구현이 없어도 됩니다. 하지만 구조적 타이핑은 객체의 행동이 아닌 객체의 구조가 해당 타입의 요구 사항을 충족하면 해당 타입으로 간주합니다.
타입 시스템의 동작
덕 타이핑은 런타임에 객체의 행동을 평가합니다. 따라서 동적 타입 언어에서 주로 사용됩니다. 구조적 타이핑은 정적 타입 시스템에서도 동작합니다. 이는 컴파일 타임에 타입 검사를 수행하며, 객체의 구조가 타입 호환성을 결정합니다. 이러한 점에서 구조적 타이핑은 정적 타입 시스템과 잘 어울리며, 코드의 안정성을 보장하는 데에 도움을 줍니다.
이러한 특성의 구조적 타이핑은 명시적인 타입 선언이나 인터페이스 구현에 대한 부담을 줄이고 코드의 유연성과 재사용성은 높여주는 장점이 있습니다. 객체의 구조를 기반으로 타입을 결정하기 때문에 코드를 직관적으로 이해할 수 있기도 합니다. 하지만 객체의 구조를 기반으로 타입을 결정하기 때문에 때로는 의도하지 않은 동작을 초래할 수 있습니다. 또한 명시적인 타입 선언이 없기 때문에 코드의 가독성이 떨어질 수 있어 사용에 주의가 필요합니다.
 

4.5 타입 별칭과 인터페이스의 차이점

4.5.1 선언 병합

인터페이스의 선언 병합은 같은 이름의 인터페이스가 여러 번 선언될 경우, 이들이 자동으로 병합되어 하나의 인터페이스를 생성하는 것을 의미합니다. 즉 같은 이름의 인터페이스 중 중복되는 멤버(프로퍼티나 메서드)가 있는 경우 새로운 내용으로 덮어쓰고, 없는 멤버의 경우 프로퍼티를 더해나가게 됩니다. 이렇게 선언 병합으로 인터페이스 프로퍼티를 더해나가는 것은 코드의 유지 보수성을 향상시킵니다.
// 인터페이스 선언 병합 interface Developer { name: string; age: number; job: string; } interface Developer { frontendSkills: string[]; } let myInfo: Developer = { name: "Wade", age: 24, job: "Frontend Developer", frontendSkills: ["HTML", "CSS", "JavaScript"], };
반면, 타입 별칭은 처음 선언될 때 형태가 고정되어 선언 병합을 지원하지 않고 같은 이름의 타입 별칭을 다시 선언하면 에러를 발생시킵니다.
// 타입 별칭 선업 병합 에러 type Developer = { name: string; age: number; job: string; }; type Developer = { frontendSkills: string[]; // Error };

4.5.2 확장과 유연성

인터페이스는 extends를 통해 다른 인터페이스를 확장할 수 있습니다. 이를 통해 기존 인터페이스의 프로퍼티를 상속받아 새로운 프로퍼티 추가할 수 있습니다.
아래의 예시에서 Developer 인터페이스는 Person 인터페이스를 확장하고 있습니다. 따라서 Developer 인터페이스는 name, age, job 프로퍼티를 상속받아 사용하고, 추가로 frontendSkills 프로퍼티를 가지고 있습니다.
// 인터페이스 extends interface Person { name: string; age: number; job: string; } interface Developer extends Person { frontendSkills: string[]; } let myInfo: Developer = { name: "Wade", age: 24, job: "Frontend Developer", frontendSkills: ["HTML", "CSS", "JavaScript"], };
타입 별칭은 type 을 사용하여 정의합니다. 한번 정의된 형태는 그 형태가 고정되어 다른 타입 별칭이나 인터페이스를 통해 확장하거나 수정할 수 없습니다. 하지만 타입 별칭은 인터섹션 & 을 통해 타입을 합하여 상속의 개념으로 새로운 타입을 정의할 수 있습니다.
// 타입 별칭 인터섹션 type Person = { name: string; age: number; job: string; }; type Stack = { frontendSkills: string[]; }; type Developer = Person & Stack; let myInfo: Developer = { name: "Wade", age: 24, job: "Frontend Developer", frontendSkills: ["HTML", "CSS", "JavaScript"], };
타입 별칭은 튜플을 선언하는 데에도 사용될 수 있습니다. 튜플은 고정된 길이의 배열을 표현하는 데 사용되며, 각 요소의 타입은 순서대로 정의됩니다.
// 튜플 type Tuple = [number, string, boolean];
따라서 유니온 타입, 튜플, 인터섹션이 필요한 경우가 아니라면 확장성이 뛰어난 인터페이스를 사용하는 것이 일반적으로 권장됩니다.

4.5.3 클래스 구현

인터페이스는 implements 통해 클래스가 특정 인터페이스를 구현하도록 강제할 수 있습니다. 이를 통해 클래스가 특정 조건을 만족하도록 보장할 수 있습니다.
interface Person { name: string; sound(): void; } class Developer implements Person { name: string; constructor(name: string) { this.name = name; } sound() { console.log("hello world"); } }
타입 별칭은 일종의 타입 체크는 수행할 수 있지만 특정 인터페이스를 구현하도록 강제할 수 없습니다.
 

4.6 객체 타입의 호환성

4.6.1 객체 타입의 호환성

1) 객체 타입의 호환성

앞서 기본 타입의 호환성에 대해서는 이미 살펴보았습니다. 기본 타입의 호환성은 특정 타입을 다른 타입으로 취급해도 괜찮은지 판단하는 것이었습니다. 이와 마찬가지로 객체 타입의 호환성이란, 한 타입의 객체를 다른 타입의 객체에 할당할 수 있는지, 한 타입이 다른 타입의 '슈퍼타입'이 될 수 있는지를 말합니다.
다음 예시와 함께 자세히 살펴보겠습니다.
type Plant = { name: string; age: number; };
Plant라는 이름의 객체 타입이 있다고 가정했습니다. 이 Plant라는 객체 타입은 name이라는 string 프로퍼티와 age라는 number 프로퍼티 두 가지를 가지고 있는 식물 타입입니다.
type Rose = { name: string; age: number; thorn: boolean; };
이번에는 식물 중에서도 특별히 Rose라는 객체 타입을 만들었습니다. 장미 역시 식물이므로, Plant 타입과 동일하게 nameage 프로퍼티를 정의합니다. 그리고 thorn이라는 추가 프로퍼티를 만들었습니다.
let plant: Plant = { name: "이름모를식물", age: 1, }; let rose: Rose = { name: "장미", age: 3, thorn: true, };
다음에는 이렇게 각각의 타입을 갖는 객체를 만들었습니다.
plant = rose; rose = plant; //Error
plant 변수에 rose변수를 집어넣으면 아무 일도 일어나지 않습니다. 그러면 반대로 한 번 해보겠습니다. rose 변수에 plant 변수의 값을 넣으려고 하면 오류가 생깁니다. 이 관계를 보고 Rose 타입을 Plant로 취급하는 건 가능하고 PlantRose타입으로 취급하는 건 안 된다는 것을 알 수 있습니다. 또한 PlantRose의 슈퍼타입이고 Rose타입은 Plant 타입의 서브 타입이라는 걸 알 수 있습니다.
💡
모든 객체 타입은 각각 다른 객체 타입과 슈퍼-서브 타입 관계를 갖습니다. 또한, 업 캐스팅은 허용하고 다운 캐스팅은 허용하지 않습니다.

2) 객체 타입의 호환성과 객체의 구조적 타이핑

타입스크립트는 객체의 타입을 정의할 때, 프로퍼티를 기준으로 객체의 구조를 정의하는 구조적 타입 시스템을 따릅니다.
위의 예시에 대입하자면 name, age가 있는 객체는 Plant 타입이라고 name, age, thorn이 있는 객체는 Rose타입이라고 보는 것입니다. 따라서 Rose 타입에 해당되는 객체는 name, age뿐만 아니라 thorn도 가진 객체이기 때문에 Plant 타입에도 해당되는 객체가 됩니다.
반대로 Plant 타입의 객체들은 모두 Rose 타입에 포함된다고 할 수 없습니다. 왜냐하면 Rose 타입의 객체가 되려면 thorn이라는 추가 프로퍼티도 가지고 있어야 되는데 Plant 타입에 해당되는 객체들 중 thorn 이라는 프로퍼티를 가지지 않은 객체도 있을 수 있기 때문입니다. 앞서 본 예시의 plant 가 이에 해당합니다.
let plant: Plant = { name: "이름모를식물", age: 1, };

4.6.2 초과 프로퍼티 검사

type Movie = { name: string; }; type RomanceMovie = { name: string; happyEnding: boolean; }; let movie: Movie = { name:"TopGun:Maverick", } let romanceMovie: RomanceMovie = { name: "Titanic", happyEnding: false, };
이번에는 다른 예시로 Movie 타입, RomanceMovie 타입과 그 타입에 해당하는 변수들을 만들었습니다. 아까 살펴본 것처럼 Movie타입에 있는 프로퍼티를 RomanceMovie 타입이 이미 가지고 있을 뿐만 아니라 추가적인 프로퍼티까지 가지고 있습니다. 따라서. Movie가 슈퍼타입이고 RomanceMovie가 서브타입이라는 걸 알 수 있습니다.
이번에는 RomanceMovie 타입의 변수를 하나 더 만들고 객체 리터럴 안에는 RomanceMovie프로퍼티들을 그대로 넣었습니다.
let anotherRomanticMovie2: Movie = { name:"Pride&Prejudice", happyEnding: true, // Error { name: string; happyEnding: boolean; } 형식은 // 'Movie' 형식에 할당할 수 없습니다. };
현재 happyEnding이라는 프로퍼티 때문에 오류가 발생하고 있습니다. Movie 타입에는 happyEnding이라는 프로퍼티가 정의되어 있지 않지만, 우리는 이전에 Movie 타입에 RomanceMovie라는 서브타입 값을 넣는 것이 가능하다는 것을 확인했습니다. 이는 업 캐스팅에 해당되기 때문입니다.
우리가 초기화를 하려고 할 때, RomanceMovie 타입의 객체를 넣으려고 하면 오류가 발생합니다. 이것은 상당히 이상한 상황입니다. 같은 동작을 하는 두 코드 중 하나는 작동하고, 다른 하나는 작동하지 않습니다. 이는 타입스크립트의 특별한 기능인 ‘초과 프로퍼티 검사’ 때문입니다.
초과 프로퍼티 검사는 변수를 초기화할 때 객체 리터럴을 사용하면 자동으로 발동하는 타입스크립트의 특별한 기능입니다. 이는 객체 리터럴이 타입에 정의된 프로퍼티 이외의 추가 프로퍼티를 가질 경우, 이를 변수에 할당하는 것을 방지하는 역할을 합니다. 따라서 객체 리터럴로 변수를 초기화하는 경우, 타입에 정의되지 않은 초과 프로퍼티를 추가하려고 하면 타입스크립트는 이를 허용하지 않습니다.
따라서 위 코드는 Movie 타입에 정의되지 않은 happyEnding 프로퍼티를 갖는 객체를 할당하려고 했으므로 초과 프로퍼티 검사에서 오류가 발생한 것입니다. 이런 초과 프로퍼티 검사는 변수를 초기화 할 때 객체 리터럴을 사용하지만 않으면 일어나지 않습니다.
let anotherRomanceMovie2: Movie = romanceMovie;
Movie 타입으로 선언된 anotherRomanceMovie2가 있습니다. anotherRomanceMovie2는 이미 선언되어 있는 romanceMovie 변수를 이용해 초기화되었습니다. 이 경우에는 초과 프로퍼티 검사가 발동하지 않아, romanceMovieMovie 타입에 없는 추가적인 프로퍼티를 가지고 있더라도 오류가 발생하지 않습니다. 따라서 이 방법을 통해 초과 프로퍼티 검사를 회피할 수 있습니다.