Menu

TypeScript 교차 타입(&): 타입 합치기 예제

교차 타입 A & B는 동시에 A이면서 B인 값을 기술하므로 양쪽의 모든 멤버를 갖습니다. &로 객체 타입을 합치는 방법, 충돌하는 속성이 never가 되는 이유, 유니언의 교차가 공통 멤버만 남기는 방식, 대신 extends를 써야 할 때를 알아봅니다.

이 페이지에는 실행 가능한 에디터가 있습니다 - 편집하고 실행하면 결과를 바로 볼 수 있습니다.

A & B로 쓰는 교차 타입은 동시에 A이면서 B인 값을 기술합니다. 객체 타입이라면 값이 양쪽의 모든 속성을 갖는다는 뜻입니다. 속성을 다시 쓰지 않고 기존 타입을 합치는 방법입니다.

두 번째 객체는 team을 빠뜨렸으므로 컴파일 오류 TS2322이고, 메시지의 다음 줄은 Property 'team' is missing ... but required in type 'Employee'라고 말합니다. Staff 값은 Person이나 Employee가 기대되는 어디에든 넘길 수 있습니다.

객체 타입 합치기

&는 타입 별칭, 인터페이스, 인라인 객체 타입을 어떻게 섞든 동작하고, 제네릭 타입 매개변수와도 동작합니다. 마지막 경우가 대체하기 어려운 곳입니다. 받은 객체가 무엇이든 속성을 더하는 함수가 그 사실을 정확히 말할 수 있습니다.

호출하는 쪽은 넘긴 값의 정확한 타입(title, words)에 더해진 두 속성까지 유지합니다. 인터페이스는 타입 매개변수를 확장할 수 없으므로 interface ... extends로는 "T가 무엇이든, 여기에 이것들을 더한 것"을 표현할 수 없습니다.

충돌하는 속성은 never가 된다

양쪽이 같은 속성을 선언하면 그 속성의 타입은 두 타입의 교차입니다. 그 타입들에 공통된 값이 없으면 속성은 never가 되고, 컴파일러는 값을 만들려고 할 때까지 아무 말도 하지 않습니다.

index.ts(7,22): error TS2322: Type 'string' is not assignable to type 'never'.

오류는 원인이 된 타입이 아니라 객체를 가리키므로, 이런 버그는 추적하는 데 시간이 걸립니다. 에디터에서 r.id에 마우스를 올리면 타입이 never로 보입니다. type Shape = { kind: "circle" } & { kind: "square" }처럼 충돌하는 속성이 리터럴 태그라면 TypeScript는 한 걸음 더 나아가 교차 타입 전체를 never로 줄입니다. 그런 값의 속성을 읽으면 이유가 설명됩니다: Property 'kind' does not exist on type 'never'. The intersection 'Shape' was reduced to 'never' because property 'kind' has conflicting types in some constituents.

interface ... extends는 같은 충돌을 선언에서 오류 TS2430으로 잡습니다. 둘 사이의 주된 실질적 차이이며, interface와 type 비교 페이지에서 나란히 비교합니다.

호환되는 겹침은 속성을 좁힌다

두 속성 타입이 겹친다면 결과는 그 겹침입니다. 오류가 아니라 유용한 동작입니다.

후반부는 &가 유니언에 하는 일을 보여 줍니다. 양쪽이 공통으로 가진 멤버만 남깁니다. 타입을 값의 집합으로 생각하면 예측할 수 있습니다. A | B는 두 집합의 합집합이고, A & B는 겹치는 부분이며, 아무것도 없는 겹침은 never입니다.

교차 타입과 유니언

이름은 집합론에서 왔고, 객체 속성에 적용하면 거꾸로 느껴집니다.

A | B(유니언)A & B(교차)
값은A 또는 BA이면서 B
허용되는 값의 집합더 큼더 작음
쓸 수 있는 속성양쪽 모두에 있는 것만어느 쪽에서든 전부
string과 numberstring | numbernever
"a" | "b"와 "b" | "c""a" | "b" | "c""b"

객체 타입의 교차가 속성을 더 많이 갖는 것은 바로 더 적은 값, 즉 모든 것을 가진 객체만 허용하기 때문입니다.

교차 타입과 extends

type C = A & Binterface C extends A, B
쓸 수 있는 대상유니언과 타입 매개변수를 포함한 모든 타입멤버가 정적으로 알려진 객체 타입
충돌하는 속성조용히 never가 됨선언에서 오류 TS2430 또는 TS2320
결과구성 요소별로 검사되는 교차 타입관계가 캐시되는 평평한 이름 있는 타입 하나
큰 조합타입 검사를 느리게 할 수 있음TypeScript 위키의 Performance 페이지가 권장

객체 타입 별칭 몇 개를 합치는 데는 &가 관용적이고 괜찮습니다. 여러 부분으로 만든 타입이나 공개 API 타입이라면 extends가 오류를 더 일찍 주고 타입 검사 비용도 적습니다.

원시 타입과의 교차: 브랜딩

원시 타입과 객체 타입을 교차해도 never가 되지 않습니다. string & { readonly __brand: "UserId" }는 컴파일 시점에만 있는 추가 표시를 단 문자열입니다. 실제 문자열에는 그런 속성이 없는데, 그것이 바로 핵심입니다. 일부러 브랜드를 단언하는 코드만 그런 값을 만들 수 있으므로, 평범한 string이나 OrderId를 UserId가 기대되는 곳에 더 이상 넘길 수 없습니다. 이 기법은 별도 페이지 브랜드 타입에서 다룹니다.

자주 묻는 질문

TypeScript에서 교차 타입이란 무엇인가요?

A & B로 쓰는 타입으로, 값이 A와 B를 동시에 만족해야 합니다. 객체 타입이라면 값이 A의 모든 속성과 B의 모든 속성을 갖는다는 뜻입니다. 두 타입 별칭을 하나로 합치는 흔한 방법입니다.

유니언과 교차 타입의 차이는 무엇인가요?

유니언 A | B는 "둘 중 하나"라는 뜻입니다. 값은 A이거나 B일 수 있고, 좁히기 전까지는 둘이 공유하는 것만 쓸 수 있습니다. 교차 A & B는 "둘 다"라는 뜻입니다. 값이 양쪽의 모든 것을 갖습니다. 객체 타입이라면 유니언은 더 많은 값을 받고, 교차 타입은 더 많은 속성을 갖습니다.

왜 내 교차 타입이 never가 되나요?

양쪽을 모두 만족하는 값이 없기 때문입니다. string & number는 never이고, { id: string } & { id: number }는 id를 string & number로 만들므로 그 속성은 never가 되어 어떤 객체도 만들 수 없습니다. 두 객체 타입이 같은 리터럴 태그를 서로 다른 값으로 가지면(kind: "circle"과 kind: "square") 교차 타입 전체가 never로 줄어듭니다.

교차 타입과 extends 중 무엇을 써야 하나요?

둘 다 객체 타입을 합칩니다. interface X extends A, B는 충돌하는 속성을 선언에서 보고하며, TypeScript 팀이 큰 객체 타입을 조합할 때 권합니다. &는 유니언과 제네릭 매개변수를 포함한 어떤 타입과도 동작하는데, extends는 이들을 합칠 수 없습니다. 타입 별칭과 제네릭 헬퍼에는 &를, 인터페이스를 만들 때는 extends를 쓰세요.

TypeScript에서 두 객체 타입을 합치려면 어떻게 하나요?

type Merged = A & B라고 씁니다. 런타임 값은 두 객체를 펼칩니다: const merged: A & B = { ...a, ...b }. A와 B가 같은 속성을 다른 타입으로 공유하면 그 속성의 타입은 never가 됩니다. 두 번째 객체의 속성이 첫 번째 것을 대체해야 한다면 Omit<A, keyof B> & B를 쓰세요.

Coddy programming languages illustration

Coddy로 코딩 배우기

시작하기