타입 별칭
type 키워드로 유니온과 인터섹션, 객체 타입에 다시 쓸 이름을 붙이고, 별칭이 새 타입을 만들지 않는다는 점과 확장 방식, 선언 병합, 표현할 수 있는 타입의 차이로 type과 interface를 고르는 기준을 정리합니다.
앞 글에서 type UserId = string | number처럼 타입에 이름을 붙여 썼습니다. 이렇게 이미 있는 타입에 새 이름을 붙이는 것을 타입 별칭(type alias)이라고 합니다. 긴 유니온이나 여러 객체를 합친 인터섹션을 매번 다시 적는 대신 짧은 이름으로 부를 수 있어서, 코드가 짧아지고 그 타입이 무엇을 뜻하는지도 드러납니다.
이 글은 타입 별칭을 만드는 방법과, 별칭이 할 수 있는 일과 할 수 없는 일을 먼저 봅니다. 그다음 객체의 모양을 나타낼 때 별칭과 인터페이스 가운데 무엇을 고를지 기준을 정리합니다. 예제는 앞 글처럼 ts-first 프로젝트에 aliases.ts를 만들고 include를 그 파일로 바꾸어 확인합니다.
TYPE / RUNTIME / CONTRACT
타입 별칭은 기존 계약에 이름을 붙인다
Name=string, Id=string|number, ThreeDPoint=Point&ZCoordinate처럼 타입 식을 재사용합니다. 별칭의 다른 이름만으로 값이 분리되거나 런타임 객체가 만들어지지는 않습니다.
ACTUAL ALIASES
Name · Age · Id에 읽기 쉬운 이름
Name=string,Age=number,Id=string|number입니다. 별칭을 붙여도 기존 허용 값과 연산은 그대로입니다. 이름이 다르다는 이유만으로 같은 string ID가 서로 구별되지는 않습니다.
ACTUAL INTERSECTION
ThreeDPoint는 x · y · z를 요구
Point의x/y와 ZCoordinate의z를 결합하고 실제 객체는10/20/30을 제공합니다. 타입 별칭이 누락된 z 값을 자동으로 채워 주지 않습니다.
ACTUAL OBJECT ALIAS
User의 선택·읽기 전용 속성
User.email은 생략할 수 있고 registrationDate의 재할당은 readonly로 제한합니다. Date 객체 내부를 동결하는 계약은 아닙니다.
ERASURE / ADDITIONAL DESIGN
검증이나 별도 의미 구분은 직접 설계
type 선언은 JavaScript에 남지 않습니다. 외부 값을 확인하는 validator나 의미가 다른 ID를 구분하는 brand는 별도로 설계하며, 이 절의 alias 예제에는 구현되어 있지 않습니다.
type은 기존 타입 식에 재사용할 이름을 붙입니다. Name·Age·Id·ThreeDPoint의 계약을 확인하며 별칭이 런타임 wrapper나 validator를 생성하지 않음을 구분합니다.
타입 별칭 만들기
타입 별칭은 type 키워드 뒤에 이름을 쓰고, 등호(=) 뒤에 그 이름이 가리킬 타입을 적어 만듭니다. 원시 타입과 유니온, 객체 타입, 인터섹션처럼 어떤 타입 식에도 이름을 붙일 수 있습니다.
type Name = string;
type Id = string | number;
type Point = { x: number; y: number };
type ZCoordinate = { z: number };
type ThreeDPoint = Point & ZCoordinate;
const myName: Name = "김타입";
const userId: Id = "user_007";
const point3d: ThreeDPoint = { x: 10, y: 20, z: 30 };
console.log(myName, userId, point3d.z);김타입 user_007 30이렇게 만든 별칭은 원래의 타입을 쓰는 자리라면 어디에나 쓸 수 있습니다. 위 그림처럼 별칭은 검사할 때 같은 타입 식을 다시 쓰게 해 주는 이름이고, 컴파일한 자바스크립트에서는 인터페이스처럼 모두 지워집니다.
별칭은 새 타입을 만들지 않고, 같은 타입에 이름을 하나 더 붙일 뿐입니다. 그래서 이름이 다른 두 별칭이 모두 number를 가리키면, 서로의 값을 그대로 주고받을 수 있습니다.
type Meters = number;
type Seconds = number;
const duration: Seconds = 5;
const distance: Meters = duration;
console.log(distance);55초를 미터 변수에 넣었는데도 검사는 통과하고, 값도 바뀌지 않은 채 5가 출력됩니다. 이름은 사람이 의도를 읽는 데 도움이 되지만, 단위가 다른 값을 막아 주지는 않습니다. 같은 number를 단위별로 구분해야 한다면 브랜드(brand) 타입 같은 별도의 기법이 필요합니다. 브랜드 타입은 type Meters = number & { readonly __brand: "Meters" }와 type Seconds = number & { readonly __brand: "Seconds" }처럼 실제 값에는 없는 표시용 속성을 타입에만 더해, 표시가 다른 두 별칭이 서로 할당되지 않게 만드는 방법이며, 값을 만들 때마다 100 as Meters 같은 단언이 필요합니다. 또 객체 모양에 붙인 별칭도 실행 중에 값을 만들거나 검사하는 함수가 되지는 않습니다.
객체 타입에 이름 붙이기
별칭은 객체 리터럴 타입에도 붙일 수 있어서, 인터페이스와 거의 같은 방식으로 객체의 모양을 나타냅니다. 선택적 속성과 읽기 전용 속성도 똑같이 쓸 수 있습니다.
type User = {
id: string;
name: string;
email?: string;
readonly registrationDate: Date;
};
const newUser: User = {
id: "u_xyz",
name: "박개발",
email: "[email protected]",
registrationDate: new Date("2024-01-01T00:00:00Z"),
};
console.log(newUser.name, newUser.registrationDate.toISOString());박개발 2024-01-01T00:00:00.000Z날짜를 Z가 붙은 협정 세계시로 정해 두었으므로, toISOString()은 컴퓨터의 시간대와 관계없이 같은 문자열을 돌려줍니다. registrationDate에는 readonly가 붙어 있어서, newUser.registrationDate = new Date();처럼 새 날짜를 넣으면 TS2540으로 진단됩니다. 다만 앞 글들에서 본 것처럼 readonly는 속성에 새 값을 넣는 것만 막으므로, 그 속성이 가리키는 Date 객체의 메서드로 날짜를 바꾸는 것까지 막지는 않습니다.
인터페이스와 타입 별칭의 차이
객체의 모양은 인터페이스로도 별칭으로도 나타낼 수 있어서 둘은 비슷해 보입니다. 하지만 확장하는 방법과 같은 이름을 다시 선언할 때의 동작, 나타낼 수 있는 타입의 범위가 다릅니다.
확장하는 방법
인터페이스는 extends로 다른 인터페이스를 확장하고, 별칭은 인터섹션(&)으로 요구를 더합니다. 두 방법 모두 결과적으로 color와 radius를 함께 요구하는 타입을 만듭니다.
interface BaseShape {
color: string;
}
interface Circle extends BaseShape {
radius: number;
}
type BaseShapeAlias = { color: string };
type CircleAlias = BaseShapeAlias & { radius: number };둘을 섞어 쓸 수도 있습니다. 멤버가 정적으로 정해진 객체 모양의 별칭은 인터페이스가 extends로 확장할 수 있고, 4장에서 다루는 클래스가 implements로 구현할 수도 있습니다. 하지만 type Id = string | number 같은 유니온 별칭은 어느 한 모양으로 정해지지 않으므로 확장하거나 구현할 수 없으며, interface Wrong extends Id {}는 An interface can only extend an object type or intersection of object types with statically known members.로 진단됩니다.
여러 객체 타입을 합칠 때는 성능의 차이도 있습니다. 공식 성능 안내는 인터페이스의 extends가 만든 관계를 컴파일러가 저장해 두고 다시 쓰는 반면, 인터섹션은 쓸 때마다 다시 계산할 수 있으므로, 객체 타입을 합칠 때는 인터페이스의 확장을 권합니다.
같은 이름의 선언
인터페이스는 같은 이름으로 여러 번 선언하면 그 선언들이 하나로 합쳐집니다. 이것을 선언 병합(declaration merging)이라고 하며, 라이브러리가 정의한 인터페이스에 속성을 더할 때 씁니다.
interface Car {
brand: string;
}
interface Car {
model: string;
}
const myCar: Car = { brand: "BMW", model: "X5" };
console.log(myCar.brand, myCar.model);BMW X5두 선언이 합쳐져 Car는 brand와 model을 모두 요구합니다. 다만 같은 이름의 함수가 아닌 속성을 다시 선언할 때는 타입이 같아야 하므로, 두 번째 선언에 brand: number를 적으면 Subsequent property declarations must have the same type.으로 시작하는 진단이 나옵니다. 메서드를 다시 선언하는 것은 이와 다른 규칙으로 합쳐지며, 그 규칙은 3장의 함수 오버로딩과 함께 이해하면 됩니다.
타입 별칭은 같은 이름으로 다시 선언할 수 없습니다. type Product = { name: string }; 다음에 type Product = { price: number };를 적으면 Duplicate identifier 'Product'.라는 TS2300이 나오므로, 별칭에 속성을 더하려면 새 이름의 별칭을 만들어 인터섹션으로 합칩니다.
나타낼 수 있는 타입
인터페이스는 객체의 모양만 나타냅니다. 원시 타입이나 유니온, 튜플 자체에 이름을 붙이는 일은 별칭만 할 수 있습니다.
type StringOrNumber = string | number;
type PointTuple = [number, number];
type ViewState = "loading" | "ready";인터페이스로 이 타입들을 직접 정의할 수는 없지만, 인터페이스의 속성 타입으로 유니온이나 튜플을 쓰는 것은 얼마든지 가능합니다. interface MyText extends string {}처럼 원시 타입을 확장하려는 선언도 진단됩니다.
TYPE / RUNTIME / CONTRACT
표현할 타입과 선언 확장 정책으로 선택한다
객체 계약은 type과 interface 둘 다 표현할 수 있습니다. 임의 타입 식이 필요한지, 같은 이름의 선언을 병합할지, 정적으로 알려진 객체 멤버를 확장·구현할지를 따로 확인합니다.
TYPE / EXPRESSION
유니온 · 튜플 · 원시 타입 별칭
StringOrNumber와 PointTuple 같은 타입 식에 이름을 붙입니다. interface로 원시 타입이나 유니온 자체를 직접 정의하지 않습니다.
INTERFACE / MERGING
같은 이름의 선언을 조건에 맞게 병합
본문 Car 두 선언은 서로 다른 brand와 model을 함께 요구합니다. 같은 이름의 비함수 속성을 반복 선언하면 타입도 같아야 하며, 단순히 한쪽 타입에 할당 가능하다는 조건으로 대신할 수 없습니다. 함수 멤버의 오버로드는 별도 규칙입니다. type Product 재선언은 진단됩니다.
BOTH / OBJECT SHAPE
객체형 alias도 확장·구현 대상
정적으로 알려진 객체 멤버를 가진 alias는 interface extends나 class implements의 대상이 될 수 있습니다. 유니온은 해당 대상이 될 수 없습니다. 실제 클래스 구현은 이 절에서 제공하지 않습니다.
EXTENDS / INTERSECTION
충돌 진단과 교차 제약을 구분
interface extends는 호환되지 않는 속성을 진단합니다. 교차 타입 &는 두 요구를 동시에 적용해 never가 될 수도 있습니다. 본문의 BaseShape/Circle과 alias 교차 예는 호환되는 color+radius입니다.
임의 타입 식과 선언 병합 요구를 구분합니다. 알려진 객체형 alias도 extends·implements의 대상이 될 수 있지만 유니온은 해당 대상이 될 수 없습니다.
위 그림은 이 차이를 고르는 기준으로 정리한 것입니다. 객체의 모양은 둘 다 나타낼 수 있으므로, 어떤 타입 식이 필요한지, 같은 이름의 선언을 합칠 필요가 있는지, 정적으로 정해진 객체를 확장하거나 구현할지를 따로 확인합니다.
더 깊이: 진단에 별칭의 이름이 남는 조건
별칭은 새 타입을 만들지 않으므로, 컴파일러가 타입을 보여 줄 때 별칭의 이름을 쓸지 펼친 모양을 쓸지는 따로 정해야 합니다. TypeScript 4.1까지는 유니온을 다른 타입과 합치면 펼쳐서 평평한 유니온 하나로 정규화했고, 그 과정에서 별칭의 이름이 사라졌습니다. 예를 들어 type BasicPrimitive = number | string | boolean을 받아 undefined도 돌려줄 수 있는 함수의 반환 타입은 BasicPrimitive | undefined가 아니라 string | number | boolean | undefined로 표시되었습니다. TypeScript 4.2부터는 타입이 원래 어떻게 쓰이고 조합되었는지를 기억해 두어, 편집기와 선언 파일 출력에서 별칭의 이름이 더 잘 남습니다.
인터페이스는 이름으로 쓴 곳이라면 진단에 언제나 그 이름 그대로 나타난다고 Handbook은 설명합니다. 같은 글은 두 방법 가운데 고를 기준이 필요하다면 별칭만의 기능이 필요해질 때까지는 인터페이스를 쓰라는 경험칙도 제시합니다.
언제 무엇을 쓸까
다른 인터페이스가 확장하거나 클래스가 구현할 객체의 모양, 또는 라이브러리 사용자가 선언 병합으로 속성을 더할 수 있어야 하는 모양에는 인터페이스가 알맞습니다. 유니온과 튜플, 리터럴 타입처럼 객체가 아닌 타입이나, 여러 타입을 조합한 식에 이름을 붙일 때는 별칭을 씁니다.
객체의 모양만 나타낸다면 두 방법 모두 쓸 수 있으므로, 어느 한쪽이 언제나 더 낫다고 정하기보다 팀의 규칙을 하나로 맞추는 것이 중요합니다. 규칙 없이 섞어 쓰면 읽는 사람이 "왜 여기는 type이고 저기는 interface인지"를 추측해야 하기 때문입니다.
정리
- 타입 별칭은
type 이름 = 타입으로 어떤 타입 식에든 이름을 붙이며, 컴파일한 자바스크립트에서는 지워집니다. - 별칭은 새 타입을 만들지 않으므로, 같은 타입을 가리키는 두 별칭의 값은 서로 할당할 수 있습니다.
- 인터페이스는
extends로, 별칭은 인터섹션으로 확장하며, 정적으로 정해진 객체 모양의 별칭은 인터페이스가 확장하거나 클래스가 구현할 수 있지만 유니온 별칭은 그럴 수 없습니다. - 인터페이스는 같은 이름의 선언을 합치고, 별칭을 다시 선언하면 TS2300이 나옵니다.
- 유니온과 튜플, 원시 타입에 이름을 붙이는 것은 별칭만 할 수 있고, 객체의 모양은 팀의 규칙에 따라 어느 쪽으로든 나타냅니다.
연습 문제
1. 읽기 전용인 날짜
위의 newUser에서 newUser.registrationDate = new Date("2026-01-01T00:00:00Z");와 newUser.registrationDate.setUTCFullYear(2026); 가운데 타입 검사를 통과하는 것은 어느 것인가요? 통과한 코드를 실행한 뒤 toISOString()의 결과는 무엇인가요?
해설 보기
첫 번째 코드는 읽기 전용 속성에 새 값을 넣으므로 Cannot assign to 'registrationDate' because it is a read-only property.로 진단됩니다. 두 번째 코드는 속성은 그대로 두고, 그 속성이 가리키는 Date 객체의 메서드를 부르므로 통과합니다. 협정 세계시의 연도만 바뀌므로 결과는 2026-01-01T00:00:00.000Z이며, readonly가 속성 안의 객체까지 얼리지 않는다는 것을 보여 줍니다.
2. 표현할 것에 맞추어 고르기
화면의 상태 "loading" | "ready"에 이름을 붙이는 타입과, 같은 이름의 선언을 두 번 써서 brand와 model을 함께 요구하는 객체 모양을 각각 만들어 보세요. 두 번째 선언이 객체를 만들어 주는지도 설명해 보세요.
해설 보기
type ScreenState = "loading" | "ready";
interface VehicleShape { brand: string; }
interface VehicleShape { model: string; }
const vehicle: VehicleShape = { brand: "예제", model: "학습용" };첫 번째는 유니온에 이름을 붙여야 하므로 별칭을, 두 번째는 같은 이름의 선언을 합쳐야 하므로 인터페이스를 고릅니다. 선언은 타입의 요구 사항을 정할 뿐 값을 만들지 않으므로, brand와 model의 실제 값은 마지막 줄의 객체 리터럴이 직접 넣어 줍니다.
3. 단위가 다른 별칭
위의 Meters와 Seconds가 있을 때, function runFor(time: Seconds) { return time * 2; }에 const trackLength: Meters = 100;을 넘기는 runFor(trackLength)는 검사를 통과하나요? 결과는 무엇인가요?
해설 보기
통과하고, 결과는 200입니다. Meters와 Seconds는 모두 number의 다른 이름일 뿐이므로, 컴파일러에게는 number를 number 매개변수에 넘기는 호출과 같습니다. 이름이 다르다고 값이 구분되지 않으므로, 단위를 실수로 섞는 것을 막으려면 별칭이 아닌 다른 방법이 필요합니다.
이것으로 2장의 기본 타입을 마칩니다. 다음 장의 함수 타입 정의에서는 함수의 매개변수와 반환값, 함수 자체의 타입을 적는 방법을 다룹니다.