08/16: 구조적 타이핑 / 명시적 타이핑의 개념 혼재부분을 수정하였습니다.
혹시 그 이전에 보셨던분이 계시다면 부정확한 정보 전달 죄송합니다..
타입스크립트는 부분적으로 구조적 타이핑을 기반으로 하는 프로그래밍 언어다.
구조적 타이핑이란 타입 시스템의 한 종류로, 두 개의 타입이 내부 구조(멤버 변수 및 함수)가 어떻게 정의되었는지에 따라 호환 가능한지를 결정한다.
즉, 명시적으로 타입을 상속하거나(명시적 타이핑) 구현하지 않아도(구조적 타이핑) 서로 다른 두 개의 타입을 비교하고 호환성을 검사할 수 있도록 해주어 유연한 프로그래밍을 가능하게 한다.
이번에 공부한 부분은 타입스크립트의 특성 중 하나로, 누락된 속성이 실제 사용되지 않을 경우, 타입스크립트는 이를 문제로 삼지 않는다는 부분이다.
구조적 타이핑을 예시를 들어보자면 법이 아닌 관용이다.
실사용과 관계 없이 모든 타입이 정확히 지켜졌는가를 따지기보다는 유연하게 대처한다.
그러나 이런 유연성 때문에 때로 의도하지 않은 호환성 문제가 발생할 수 있다.
따라서 프로젝트에서 구조적 타이핑을 사용할 때는 타입의 구조와 호환성에 대한 이해가 중요하다.
공부한 부분은 유니온과 제네릭을 사용할 때 발생할 수 있는 문제점과 그 해결방법이다.
enum Days { Sunday,Monday,Tuesday,Wednesday,Thursday,Friday,Saturday }
interface User {
name: string;
age: number;
}
interface Volunteer extends User {
gender: string;
availableDay: Partial<Days>[];
}
interface Expert extends User {
occupation: string;
}
type Part = Volunteer | Expert;
type VolunteerTeam<T extends User> = T[];
// User 타입을 만족하는 것을 기본 전제로 제네릭 타입까지 만족해야 한다.
const GoodNeighbor: VolunteerTeam<Part> = [
// extends를 사용하여 명시적으로 타이핑하여도 Part Type의 한가지 조건에 만족하면 되는 문제로 인해 허용된다.
{
name: '김*성',
age: 34,
gender: '남자',
availableDay: [Days.Friday, Days.Saturday, Days.Sunday],
occupation: '구호활동가',
},
// extends를 사용하여 명시적으로 타이핑하여도 Part Type의 한가지 조건에 만족하면 되는 문제로 인해 허용된다.
{
name: '최*민',
age: 28,
gender: '남자',
occupation: '의사',
},
{
name: '이*택',
age: 27,
gender: '남자',
availableDay: [Days.Monday],
},
{
name: '장*찬',
age: 27,
occupation: '과학자',
},
]
GoodNeighbor.forEach((volunteer) => {
// Type Error =>
// 'Volunteer | Expert' 형식에 'occupation' 속성이 없습니다. 'Volunteer' 형식에 'occupation' 속성이 없습니다.
console.log(volunteer.occupation);
})
VolunteerTeam<Part>는 각 배열 내 요소의 속성에
1. User의 속성이 모두 존재하는지(실제 상속 여부)
2. 제네릭에 사용된 Volunteer 또는 Expert의 속성을 포함하고 있는지(Generic 사용 여부)
3. 그 이외에 다른 명시되지 않은 속성을 포함하고 있는지(기본 typing)
를 확인한다.
배열 내 객체들은 모두 1,2,3번을 충족하고 있다.
그렇기 때문에 각각의 맴버들이 조건에 일치하므로 호환 가능한 타입으로 간주된다.
누락된 속성은 사용되지 않을 경우에는 에러가 발생하지 않지만, 사용하려 할 때 에러가 발생한다.(위 코드의 console.log에서 발생)
인터페이스마다 어떤 속성을 갖추어야 하는지 속성으로 타입을 지정하여 문제를 사전에 방지한다.
interface Volunteer2 extends User {
type: 'volunteer'; // type 속성 추가
gender: string;
availableDay: Partial<Days>[];
}
interface Expert2 extends User {
type: 'expert'; // type 속성 추가
occupation: string;
}
type Part2 = Volunteer2 | Expert2;
type VolunteerTeam2<T extends User> = T[];
// 배열 내 객체에 type 속성이 사용되지 않는다면 에러가 발생한다.
const GoodNeighbor2: VolunteerTeam<Part2> = [
// 'type' 속성이 '{...아래 객체}' 형식에 없지만 'Volunteer2' 형식에서 필수입니다.ts(2322)
{
name: '김명성',
age: 34,
gender: '남자',
availableDay: [Days.Friday, Days.Saturday, Days.Sunday],
occupation: '구호활동가',
},
//'type' 속성이 '{ ...아래 객체 }' 형식에 없지만 'Expert2' 형식에서 필수입니다.ts(2322)
{
name: '최*민',
age: 28,
gender: '남자',
occupation: '의사',
},
{
name: '이*택',
age: 27,
gender: '남자',
availableDay: [Days.Monday],
},
{
name: '장*찬',
age: 27,
occupation: '과학자',
},
]
타입 가드,네로잉은 타입을 좁히는 역할을 한다.
type VolunteerTeam3<T extends User> = T[];
type Part3 = Volunteer3 | Expert3;
const GoodNeighbor3: VolunteerTeam<Part3> = [
// 'string' 형식은 'undefined' 형식에 할당할 수 없습니다.ts(2322)
{
name: '김*성',
age: 34,
gender: '남자',
availableDay: [Days.Friday, Days.Saturday, Days.Sunday],
occupation: '구호활동가',
},
// 'string' 형식은 'undefined' 형식에 할당할 수 없습니다.ts(2322)
{
name: '최*민',
age: 28,
gender: '남자',
occupation: '의사',
},
// Volunteer3
{
name: '이*택',
age: 27,
gender: '남자',
availableDay: [Days.Monday],
},
// Expert3
{
name: '장*찬',
age: 27,
occupation: '과학자',
},
]
Volunteer,Expert 타입에 상반되는 속성을 갖지 못하게 never 타입으로 지정하여 객체가 가질 수 있는 타입 문제를 사전에 방지할 수 있다.
참고로 Omit과 Exclude는 유연성 문제를 해결하지 못한다.
interface Volunteer extends User {
gender: string;
availableDay: Partial<Days>[];
}
interface Expert extends User {
occupation: string;
}
type OmittedVolunteer = Omit<Volunteer, "occupation">;
type OmittedExpert = Omit<Expert, "gender" | "availableDay">;
type Part = OmittedVolunteer | OmittedExpert;
const whatismytype: Part = {
name: '김*성',
age: 34,
gender: '남자',
availableDay: [Days.Friday, Days.Saturday, Days.Sunday],
occupation: '의사'
}
// 사용시 에러발생
// 'Part' 형식에 'occupation' 속성이 없습니다.
console.log(whatismytype.occupation);
두 방법 모두 상황에 따라 다르게 사용하겠지만 주로 1번 방법이 보편적일 것 같으며 Error 문구 또한 1번 type 속성을 추가하는 방법이 제일 정확히 알려주는 것 같다.
개발자로서 배울 점이 많은 글이었습니다. 감사합니다.