구조적 타이핑 : 어 난 티니핑 멤버 아닌데..
https://toss.tech/article/typescript-type-compatibility글과
Effective 타입스크립트 책을 보고 정리 하였습니다 !
덕 타이핑은 "만약 어떤 것이 오리처럼 걷고, 헤엄치고, 꽥꽥거린다면 그것은 오리일 것이다"라는 철학에서 유래된 개념입니다. 즉, 객체가 어떤 인터페이스나 타입을 명시적으로 구현하지 않더라도, 해당 객체가 요구되는 기능을 모두 구현하고 있다면 그 객체를 사용할 수 있다는 의미입니다.
타입스크립트에서도, 객체가 특정 속성과 메서드를 가지고 있다면 타입 선언 없이도 그 객체가 예상대로 동작할 수 있습니다. 이것이 타입스크립트의 구조적 서브타이핑 방식과 연결됩니다.
덕 타이핑에서 아이디어를 차용해, 어떤 글자 뒤에 "핑"을 붙이면 그 글자가 <<캐치! 티니핑>>의 멤버가 된다는 발상을 농담처럼 표현해보았습니다
function isTiniPing(name: string): boolean {
return name.endsWith('핑');
}
const name = "하츄핑";
if (isTiniPing(name)) {
console.log(`${name}은(는) 티니핑 멤버입니다!`);
}
🤔 그렇다면 서브타이핑의 종류에는 어떤 것이 있을까요 ??
타입의 계충 구조에서 한 타입이 다른 타입의 부분 집합일 때 발생하는 타입 관계
타입 정의 시에 상속 관계임을 명확히 명시한 경우에만 타입 호환을 허용하는 것입니다.
- 개발자의 명확한 의도를 반영할 수 있다
- 오류 발생할 가능성 배제
상속 관계가 명시되어 있지 않더라도 객체의 프로퍼티를 기반으로 사용처에서 사용함에 문제가 없다면 타입 호환을 허용하는 방식
- 객체의 프로퍼티를 체크해주는 과정 수행
- 상속관계를 명시해줄 필요가 없음
🤔 타입스크립트에서 사용하는 구조적 타이핑의 예시를 알아보겠습니다
자바스크립트와 타입스크립트는 기본적으로 타입 시스템이 열려있는(open) 상태
즉, 객체가 타입에 명시된 속성 외에도 추가적인 속성을 가질 수 있으며, 타입 체크는 해당 속성들만 맞는지를 확인합니다
interface Vector2D {
x: number;
y: number;
}
function calculateLength(v: Vector2D) {
return Math.sqrt(v.x * v.x + v.y * v.y);
}
interface NamedVector {
name: string;
x: number;
y: number;
}
//calculateLength 함수에 NamedVector 타입의 객체 v를 전달할 수 있다.
// 왜냐하면, v가 Vector2D에서 요구하는 x와 y 속성을 가지고 있음.
const v: NamedVector = { x: 3, y: 4, name: "zee" };
console.log(calculateLength(v)); // 5
interface Vector2D {
x: number;
y: number;
}
const vector = { x: 3, y: 4, z: 5 }; // z는 추가적인 속성
function calculateLength(v: Vector2D) {
return Math.sqrt(v.x * v.x + v.y * v.y);
}
console.log(calculateLength(vector)); // 5, z는 무시됨
interface Point3D {
x: number;
y: number;
z: number;
}
function calculate2DLength(point: { x: number; y: number }) {
return Math.sqrt(point.x * point.x + point.y * point.y);
}
const point3D: Point3D = { x: 5, y: 12, z: 7 };
console.log(calculate2DLength(point3D)); // 13
type이름이 티니핑인데 속성이 눈코입이면 눈코입 속성을 가진 다른 캐릭터 또한 들어올 수 있게됩니다 때문에 type 속성에는 들어오지만
- 명확한 경우
- 애매한 경우
가 공존할 수 있게 됩니다 👉 그래서 타입 구분을 통해 이를 해결할 수 있습니다
: 캐릭터 속성에서 티니핑 타입을 정의하여 구분할 수 있습니다
// 기본 캐릭터 속성 정의
type Character = {
strength: number;
agility: number;
intelligence: number;
};
// 티니핑 타입 정의 (Character에 추가 속성 포함)
type 티니핑 = Character & {
characterBrand: string;
};
// 티니핑 객체 예시
const character1: 티니핑 = {
strength: 29,
agility: 48,
intelligence: 13,
characterBrand: '캐치! 티니핑'
};
// 일반 캐릭터와 티니핑 캐릭터 구분
const character2: Character = {
strength: 25,
agility: 40,
intelligence: 15
};
console.log(character1.characterBrand); // "캐치! 티니핑"
그렇다면 타입이 아닌 character2에만 가능한 characterBrand 프로퍼티 추가를 한다면 ?
const character2 = Character({
strength: 12;
agility: 32;
intelligence: 434;
characterBrand: '캐치! 티니핑'
}) /** 타임검사 결과 : 오류 있음 */
//다음처럼 함수에 들어온 인자가 fresh하면 해당 함수에서만 사용되고 다른 곳에서 사용되지 않기에 오류로 지정
TypeScript Type Checker는 구조적 서브타이핑을 기반으로 타입 호환을 판단하되, Freshness에 따라 예외를 둔다고 합니다.
의도적으로 __brand
와 같은 프로퍼티를 추가시켜, 개발자가 함수의 매개변수로 정의한 타입 외에는 호환이 될 수 없도록 강제하는 기법
2-1 . 타입에 고유한 식별자를 부여하는 방식
: 브랜트 타입에 브랜드 속성을 '캐치! 티니핑’으로 부여하여 티니핑 타입을 정의할 수 있습니다
type Brand<K, T> = K & { __brand: T };
// 티니핑 타입 정의
type 티니핑 = Brand<{
strength: number;
agility: number;
intelligence: number;
}, '캐치! 티니핑'>;
// 티니핑의 능력치를 계산하는 함수
function calculatePower(character: 티니핑): number {
return character.strength * 2 + character.agility * 1.5 + character.intelligence * 3;
}
// 정석적인 방법으로 브랜디드 타입 사용
const brandedCharacter2 = {
strength: 29,
agility: 48,
intelligence: 13,
__brand: '캐치! 티니핑' as const // __brand 속성 추가로 명확하게 타입 지정
};
console.log(calculatePower(brandedCharacter2)); // OK
2-2. 함수에서의 타입 제한
: 어떤 글자가 들어올 때 핑으로 끝나더라도 타입을 통해 티니핑 캐릭터인지 확인할 수 있습니다
// Character type 정의
type Character<K, T> = K & { _Character: T };
// 티니핑 타입 정의
type 티니핑 = Character<string, '티니핑'>;
// 마지막 글자가 '핑'으로 끝나는지 체크하는 함수
function is티니핑(name: string): name is 티니핑 {
return name.endsWith('핑');
}
// 티니핑 캐릭터만 허용하는 함수
//매개변수 character타입 외 제한
function introduceCharacter(character: 티니핑) {
console.log(`안녕하세요, 저는 ${character}입니다!`);
}
// 일반 문자열과 티니핑 문자열 예시
const Character1 = "구조적타이핑";
const Character2 = "하츄핑" as const;
// 타입 검사
if (is티니핑(tinipingCharacter)) {
introduceCharacter(tinipingCharacter); // OK
} else {
console.log(`${tinipingCharacter}는 티니핑이 아닙니다.`);
}
if (is티니핑(normalCharacter)) {
introduceCharacter(normalCharacter); // 타입 오류 발생, normalCharacter는 티니핑이 아님
} else {
console.log(`${normalCharacter}는 티니핑이 아닙니다.`);
}
//result
안녕하세요, 저는 하츄핑입니다!
구조적타이핑는 티니핑이 아닙니다.
객체가 고정된 속성 외에도 임의의 속성을 가질 수 있음을 타입스크립트에게 알려주는 방법
🤔 티니핑의 종류에 대해서 알아보자면..
티니핑에서 가장 우수한 티니핑으로 로열 티니핑 6종이 있다고 합니다
뿐만 아니라 레전드 티니핑 , 일반 티니핑, 빌런 티니핑 등 여러 종류가 있습니다 !
해당 속성을 옵션 속성을 통해 추가로 알려줄 수 있습니다
그럼 다음과 같이 기본 타입을 확장할 수 있습니다
// 기본 티니핑 타입 정의
interface 티니핑 {
name: string;
strength: number;
agility: number;
intelligence: number;
[key: string]: number | string | boolean | undefined; // 추가적인 속성 허용
type: string; // 티니핑 종류 (일반, 레전드, 빌런 등)
}
// 레전드 티니핑 타입 정의 (티니핑을 확장)
interface 레전드티니핑 extends 티니핑 {
legendaryPower: string; // 레전드 티니핑만의 특별한 능력
}
// 빌런 티니핑 타입 정의 (티니핑을 확장)
interface 빌런티니핑 extends 티니핑 {
evilPower: string; // 빌런 티니핑만의 특별한 능력
invertedTrianglePattern: boolean;
// 빌런 티니핑은 뿌뿌핑, 트러핑을 제외하면 눈밑에 역삼각형 무늬가 있음
}
// 일반 티니핑 타입 정의 (특별한 속성 없이 기본 티니핑 속성만 가짐)
type 일반티니핑 = 티니핑;
// 일반 티니핑 객체
const 일반TiniPing: 일반티니핑 = {
name: "키키핑",
strength: 30,
agility: 35,
intelligence: 40,
type: "일반",
};
// 레전드 티니핑 객체
const 레전드TiniPing: 레전드티니핑 = {
name: "행운핑",
strength: 80,
agility: 70,
intelligence: 90,
type: "레전드",
legendaryPower: "행운전달🍀", // 레전드 티니핑만의 특별한 능력
};
// 빌런 티니핑 객체
const 빌런TiniPing: 빌런티니핑 = {
name: "악동핑",
strength: 60,
agility: 55,
intelligence: 65,
type: "빌런",
evilPower: "번개발사⚡️", // 빌런 티니핑만의 특별한 능력
invertedTrianglePattern: true, // 역삼각형 무늬 여부
};
// 티니핑의 능력치를 계산하는 함수
function calculatePower(tiniPing: 티니핑): number {
return tiniPing.strength * 2 + tiniPing.agility * 1.5 + tiniPing.intelligence * 3;
}
// 티니핑을 소개하는 함수
function introduceTiniPing(tiniPing: 티니핑) {
console.log(`안녕하세요, 저는 ${tiniPing.name}입니다. 저는 ${tiniPing.type} 티니핑이에요!`);
if ('legendaryPower' in tiniPing) {
console.log(`저의 전설적인 능력은 ${tiniPing.legendaryPower}입니다!`);
}
if ('evilPower' in tiniPing) {
console.log(`저의 악당 능력은 ${tiniPing.evilPower}입니다!`);
if ((tiniPing as 빌런티니핑).invertedTrianglePattern) {
console.log("저는 역삼각형 무늬를 가진 빌런 티니핑입니다!");
}
}
console.log(`능력치 합계: ${calculatePower(tiniPing)}`);
}
// 티니핑 객체 출력
introduceTiniPing(일반TiniPing); // 일반 티니핑 소개
introduceTiniPing(레전드TiniPing); // 레전드 티니핑 소개
introduceTiniPing(빌런TiniPing); // 빌런 티니핑 소개
//strength: number; agility: number; intelligence: number;는 임의로 지정한 것 입니다 !
방식 | 구분 방법 | 사용 목적 |
---|---|---|
변수 타입 지정 | 타입 이름 | 같은 구조의 데이터를 명확한 상황에 맞게 구분할 때 사용 |
Branded Type | 고유한 식별자(__brand) | 같은 구조라도 엄격한 타입 구분이 필요할 때 사용 |
Index Signature | 속성의 타입만 제한 | 객체의 속성 이름을 동적으로 확장할 때 사용 |
📚 핑으로만 끝난다고 혹은 모든 티니핑이 일반 티니핑은 아니듯이 타입 구분의 필요성을 느꼈습니다!!
다음 코드에서 calculateLength
함수에 NamedVector
타입의 객체를 전달할 수 있는 이유는 무엇일까요?
interface Vector2D {
x: number;
y: number;
}
function calculateLength(v: Vector2D) {
return Math.sqrt(v.x * v.x + v.y * v.y);
}
interface NamedVector {
name: string;
x: number;
y: number;
}
const v: NamedVector = { name: "zee", x: 3, y: 4 };
console.log(calculateLength(v)); // 결과: 5
Vector2D
와 NamedVector
는 명시적으로 상속 관계에 있고 NamedVector
는 Vector2D
에서 요구하는 모든 속성(x
, y
)을 가지고 있기 때문 TypeScript
는 항상 모든 객체를 호환 가능하게 처리하기 때문 NamedVector
의 구조가 더 복잡하므로 자동으로 호환됨다음 언어 중 명목적 서브타이핑(Nominal Subtyping)을 사용하는 언어는 무엇일까요?
다음 코드를 실행할 때 타입 오류가 발생하는 이유는 무엇일까요?
type Brand<K, T> = K & { __brand: T };
type Food = Brand<{
protein: number;
carbohydrates: number;
fat: number;
}, 'Food'>;
function calcCalory(food: Food) {
return food.protein * 4 + food.carbohydrates * 4 + food.fat * 9;
}
const burger = {
protein: 100,
carbohydrates: 100,
fat: 100,
burgerBrand: '버거킹'
};
calcCalory(burger); // 오류 발생
Food
타입과 burger
타입이 구조적으로 일치하지 않기 때문에burger
에 __brand: 'Food'
가 없기 때문에calcCalory
함수는 매개변수로 burgerBrand
를 허용하지 않기 때문에FlexibleFood 인터페이스가 Food와 다른 점은 무엇일까요?
interface Food {
protein: number;
carbohydrates: number;
fat: number;
}
interface FlexibleFood {
protein: number;
carbohydrates: number;
fat: number;
[key: string]: number | string; // 추가 속성 허용
}
Food
는 고정된 속성만 가질 수 있다 FlexibleFood
는 Food
와 달리 추가적인 속성을 허용한다 FlexibleFood
는 타입 호환성 문제를 발생시킨다 FlexibleFood
는 Food
를 상속한 타입이다
매우 유용한 글입니다.