一。개요
CSS 의 선언 병합과 유사합니다:
.box {
background: red;
}
.box {
color: white;
}
/* 동치 */
.box {
background: red;
color: white;
}
TypeScript 에도 이러한 메커니즘이 있습니다:
interface IPerson {
name: string;
}
interface IPerson {
age: number;
}
// 동치
interface IPerson {
name: string;
age: number;
}
간단히 말해, 동일한 것을 설명하는 여러 선언은 하나로 병합됩니다
二。기본 개념
TypeScript 에서, 하나의 선언은 네임스페이스, 타입 또는 값을 생성할 수 있습니다. 예를 들어 Class 를 선언하면 동시에 타입과 값이 생성됩니다:
class Greeter {
static standardGreeting = "Hello, there";
greeting: string;
constructor(message: string) {
this.greeting = message;
}
greet() {
return "Hello, " + this.greeting;
}
}
let greeter: Greeter; // Greeter 타입
greeter = new Greeter("world"); // Greeter 값
([클래스와 타입](/articles/클래스-typescript 노트 4/#articleHeader8) 에서 발췌)
따라서 선언을 3 가지 카테고리로 분류할 수 있습니다:
-
네임스페이스를 생성하는 선언: 도트(
.)로 액세스하는 네임스페이스 이름 생성 -
타입을 생성하는 선언: 지정된「형태」의 타입을 생성하고 주어진 이름으로 명명
-
값을 생성하는 선언: 값을 생성하며, 출력되는 JavaScript 에도 존재
구체적으로, TypeScript 의 7 가지 선언 중에서 네임스페이스는 네임스페이스와 값 의미를 가지며, 클래스와 열거는 동시에 타입과 값 의미를 가지고, 인터페이스와 타입 별칭은 타입만, 함수와 변수는 값만 의미를 가집니다:
| Declaration Type | Namespace | Type | Value |
|---|---|---|---|
| Namespace | X | X | |
| Class | X | X | |
| Enum | X | X | |
| Interface | X | ||
| Type Alias | X | ||
| Function | X | ||
| Variable | X |
三。인터페이스 병합
가장シンプル하고 가장 일반적인 선언 병합은 인터페이스 병합으로, 기본 규칙은 동일 이름 인터페이스의 멤버를 함께 하는 것입니다:
interface Box {
height: number;
width: number;
}
interface Box {
scale: number;
}
// 동치
interface MergedBox {
height: number;
width: number;
scale: number;
}
let box: Box = {height: 5, width: 6, scale: 10};
let b: MergedBox = box;
비함수 멤버는 유일해야 합니다. 유일하지 않은 경우, 타입이 같은 함수 멤버는 무시되고 타입이 다르면 컴파일 오류가 스로우됩니다:
interface Box {
color: string
}
// 오류 Subsequent property declarations must have the same type.
interface Box {
color: number
}
함수 멤버의 경우, 동일 이름의 것은 [함수 오버로드](/articles/함수-typescript 노트 5/#articleHeader9) 로 간주됩니다:
class Animal { }
class Sheep { }
class Dog { }
class Cat { }
interface Cloner {
clone(animal: Animal): Animal;
}
interface Cloner {
clone(animal: Sheep): Sheep;
}
interface Cloner {
clone(animal: Dog): Dog;
clone(animal: Cat): Cat;
}
이는 병합됩니다:
interface Cloner {
clone(animal: Dog): Dog;
clone(animal: Cat): Cat;
clone(animal: Sheep): Sheep;
clone(animal: Animal): Animal;
}
동일 선언 내의 병합 후에도 선언 순서를 유지하며, 서로 다른 선언 간에는후선언이 우선됩니다(즉, 뒤의 인터페이스 선언문에서 정의된 함수 멤버는 병합 결과에서 앞에 옴). 비함수 멤버는 병합 후 사전순으로 정렬됩니다
특별히, 함수 시그니처가 문자열 리터럴 타입의 파라미터를 포함하는 경우, 병합 후 오버로드 리스트의 선두에 배치됩니다:
interface IDocument {
createElement(tagName: any): Element;
}
interface IDocument {
createElement(tagName: "div"): HTMLDivElement;
createElement(tagName: "span"): HTMLSpanElement;
}
interface IDocument {
createElement(tagName: string): HTMLElement;
createElement(tagName: "canvas"): HTMLCanvasElement;
}
병합 결과:
interface IDocument {
// 특수 시그니처 선두
createElement(tagName: "div"): HTMLDivElement;
createElement(tagName: "span"): HTMLSpanElement;
createElement(tagName: "canvas"): HTMLCanvasElement;
// 아래 2 개는 계속 후선언 우선에 따름
createElement(tagName: string): HTMLElement;
createElement(tagName: "canvas"): HTMLCanvasElement;
}
四。네임스페이스 병합
인터페이스와 마찬가지로, 여러 동일 이름 네임스페이스도 멤버 병합이 발생합니다. 특별한 점은 네임스페이스가 값 의미도 가진다는 것으로, 상황이 조금 복잡합니다
-
네임스페이스 병합: 각(동일 이름)네임스페이스가 공개하는 인터페이스를 병합하고, 동시에 단일 네임스페이스 내부에서도 인터페이스 병합
-
값 병합: 후선언 네임스페이스에서 공개된 멤버를 선선언에 추가
예를 들어:
namespace Animals {
export class Zebra { }
}
namespace Animals {
export interface Legged { numberOfLegs: number; }
export class Dog { }
}
// 동치
namespace Animals {
export interface Legged { numberOfLegs: number; }
export class Zebra { }
export class Dog { }
}
특별히, 공개되지 않은 멤버(non-exported member)는 소스 네임스페이스 내에서만 가시 입니다(네임스페이스 병합 메커니즘이 존재해도):
namespace Animal {
let haveWings = true;
export function animalsHaveWings() {
return haveWings;
}
}
namespace Animal {
export function doAnimalsHaveWings() {
// 오류 Cannot find name 'haveWings'.
return haveWings;
}
}
네임스페이스는 스코프 분리를 가지므로, 공개되지 않은 멤버는 네임스페이스에挂载되지 않습니다:
var Animal;
(function (Animal) {
var haveWings = true;
function animalsHaveWings() {
return haveWings;
}
Animal.animalsHaveWings = animalsHaveWings;
})(Animal || (Animal = {}));
(function (Animal) {
function doAnimalsHaveWings() {
// 오류 Cannot find name 'haveWings'.
return haveWings;
}
Animal.doAnimalsHaveWings = doAnimalsHaveWings;
})(Animal || (Animal = {}));
클래스, 함수 및 열거와의 병합
다른 네임스페이스와 병합할 수 있을 뿐만 아니라, 네임스페이스는 클래스, 함수 및 열거와도 병합할 수 있습니다
이 능력은 기존 클래스, 함수, 열거를(타입상에서)확장할 수 있게 하여, JavaScript 의 일반적인 패턴을 설명하는 데 사용됩니다. 예를 들어 클래스에 정적 멤버를 추가하거나, 함수에 정적 속성을 추가하는 등
P.S. 네임스페이스 선언은 나중에 나타나야 합니다.否则 오류:
// 오류 A namespace declaration cannot be located prior to a class or function with which it is merged.
namespace A {
function f() { }
}
class A {
fn() { }
}
병합이 아닌 오버라이드가 발생하기 때문에:
// 컴파일 결과
var A;
(function (A) {
function f() { }
})(A || (A = {}));
var A = /** @class */ (function () {
function A() {
}
A.prototype.fn = function () { };
return A;
}());
클래스와 병합
네임스페이스를 통해 기존 Class 에 정적 멤버를 추가할 수 있습니다. 예를 들어:
class Album {
label: Album.AlbumLabel;
}
namespace Album {
export class AlbumLabel { }
}
네임스페이스 간 병합 규칙과 일치하므로, class AlbumLabel 을 공개하여 다른 선언의 멤버가 액세스할 수 있도록 해야 합니다
함수와 병합
네임스페이스와 클래스 병합과 마찬가지로, 함수와 병합은 기존 함수에 정적 속성을 확장할 수 있습니다:
function buildLabel(name: string): string {
return buildLabel.prefix + name + buildLabel.suffix;
}
namespace buildLabel {
export let suffix = "";
export let prefix = "Hello, ";
}
// test
buildLabel('Lily') === "Hello, Lily"
열거와 병합
enum Color {
red = 1,
green = 2,
blue = 4
}
namespace Color {
export function mixColor(colorName: string) {
if (colorName == "yellow") {
return Color.red + Color.green;
}
else {
return -1;
}
}
}
// test
Color.mixColor('white');
열거에 정적 메서드를 갖는 것은 이상해 보이지만, JavaScript 에는 실제로 유사한 시나리오가 존재하며, 속성 집합에 동작을 추가하는 것과 같습니다:
// JavaScript
const Color = {
red: 1,
green: 2,
blue: 4
};
Color.mixColor = function(colorName) {/* ... */};
五.Class Mixin
클래스 선언은 다른 클래스나 변수 선언과 병합하지 않지만, Class Mixin 을 통해 유사한 효과를 달성할 수 있습니다:
function applyMixins(derivedCtor: any, baseCtors: any[]) {
baseCtors.forEach(baseCtor => {
Object.getOwnPropertyNames(baseCtor.prototype).forEach(name => {
Object.defineProperty(derivedCtor.prototype, name, Object.getOwnPropertyDescriptor(baseCtor.prototype, name));
});
});
}
툴 함수를 통해 다른 클래스 프로토타입 위의 것을 타겟 클래스 프로토타입에 붙여, 다른 클래스의 능력(동작)을 갖게 합니다:
class Editable {
public value: string;
input(s: string) { this.value = s; }
}
class Focusable {
focus() { console.log('Focused'); }
blur() { console.log('Blured'); }
}
// 다른 클래스에서 타입 획득
class Input implements Editable, Focusable {
// 구현 대기 Editable 인터페이스
value: string;
input: (s: string) => void;
// 구현 대기 Focusable 인터페이스
focus: () => void;
blur: () => void;
}
// 다른 클래스에서 동작 획득
applyMixins(Input, [Editable, Focusable]);
// log 'Focused'
new Input().focus();
P.S. 그 중implements Editable, Focusable 는 소스 클래스의 타입을 획득하며, 인터페이스와 유사합니다. 자세한 내용은 Interfaces Extending Classes 참조
六。모듈 확장
// 소스 파일 observable.js
export class Observable {
constructor(source) { this.source = source; }
}
// 소스 파일 map.js
import { Observable } from "./observable";
Observable.prototype.map = function (f) {
return new Observable(f(this.source));
}
이러한 모듈 확장 방식은 JavaScript 에서 매우 일반적이지만, TypeScript 에서는 오류가 보고됩니다:
// 소스 파일 observable.ts
export class Observable<T> {
constructor(public source: T) { }
}
// 소스 파일 map.ts
import { Observable } from "./observable";
// 오류 Property 'map' does not exist on type 'Observable<any>'.
Observable.prototype.map = function (f) {
return new Observable(f(this.source));
}
이때 모듈 확장(module augmentation)을 통해 컴파일러(타입 시스템)에 모듈에 추가된 멤버를 알릴 수 있습니다:
// 소스 파일 map.ts
import { Observable } from "./observable";
// 모듈 확장
declare module "./observable" {
interface Observable<T> {
map<U>(f: (x: T) => U): Observable<U>;
}
}
Observable.prototype.map = function (f) {/* ... */}
그 중, 모듈 이름의 해석 방식은import/export 와 일치하며, 자세한 내용은 [모듈 해석 메커니즘_TypeScript 노트 14](/articles/모듈 해석 메커니즘-typescript 노트 14/) 참조. 모듈 선언에 추가된 확장 멤버는 소스 모듈에 병합됩니다(마치 원래 같은 파일에 선언되어 있던 것처럼). 이 방식으로 기존 모듈을 확장할 수 있지만, 2 가지 제한 이 있습니다:
-
모듈 확장에 톱레벨 선언을 추가할 수 없으며, 기존 선언만 확장 가능
-
기본 내보내기를 확장할 수 없으며, 이름付き 내보내기만 확장 가능(
default는 예약어이므로 이름으로 확장할 수 없기 때문. 자세한 내용은 Can not declaration merging for default exported class 참조)
P.S. 위 예는 Playground 등의 환경에서declare module "./observable" 오류에 직면할 수 있습니다:
Invalid module name in augmentation, module './observable' cannot be found.
Ambient module declaration cannot specify relative module name.
모듈 파일이 존재하지 않기 때문이며, 실제 파일 모듈에서는 정상적으로 컴파일할 수 있습니다
글로벌 확장
유사한 방식으로「글로벌 모듈」(즉 글로벌 스코프 아래의 것을 수정)도 확장할 수 있습니다. 예를 들어:
// 소스 파일 observable.ts
export class Observable<T> {
constructor(public source: T) { }
}
declare global {
interface Array<T> {
toObservable(): Observable<T>;
}
}
Array.prototype.toObservable = function () {
return new Observable(this);
}
declare global 는 글로벌 스코프를 확장함을 나타내며, 추가된 것은Array 등의 글로벌 선언에 병합됩니다
아직 댓글이 없습니다