본문으로 건너뛰기

선언 병합_TypeScript 노트 16

무료2019-04-21#TypeScript#TypeScript扩展声明#TypeScript重写声明#TypeScript修改声明#TypeScript mixin#TypeScript声明合并规则

선언 병합의 의의는 기존 (타입) 선언을 확장할 수 있다는 것입니다

一。개요

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 TypeNamespaceTypeValue
NamespaceXX
ClassXX
EnumXX
InterfaceX
Type AliasX
FunctionX
VariableX

三。인터페이스 병합

가장シンプル하고 가장 일반적인 선언 병합은 인터페이스 병합으로, 기본 규칙은 동일 이름 인터페이스의 멤버를 함께 하는 것입니다:

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 등의 글로벌 선언에 병합됩니다

참고 자료

댓글

아직 댓글이 없습니다

댓글 작성