본문 바로가기
창고(2021년 이전)

[Typescript] Interface

by 측면삼각근 2020. 3. 15.
728x90
반응형

이 글은 TypeScript : 인터페이스(Interface) 를 보고 이해하기 위해 요약한 글입니다.


인터페이스는 일반적으로 타입 체크를 위해 사용되며, 변수, 함수, 클래스에 사용할 수 있다. 인터페이스는 여러가지 타입을 갖는 프로퍼티로 이루어진 새로운 타입을 정의하는 것과 유사하다. 인터페이스에 선언된 프로퍼티 또는 메소드의 구현 강제하여 일관성을 유지할 수 있도록 하는 것이다. ES6는 인터페이스를 지원하지 않지만 TypeScript는 인터페이스를 지원한다.

인터페이스는 프로퍼티와 메소드를 가질 수 잇다는 점에서 클래스와 유사하나, 직접 인스턴스를 생성할 수 없고 모든 메소드는 추상 메소드이다. 단, 추상 클래스의 추상 메소드와 달리 abstract키워드를 사용하지 않는다.

Abstract Class

인터페이스는 클래스에서 구현부가 빠져싸독 이해하면 편하다. 즉, 어떠한 객체가 이러이러한 프로퍼티 혹은 메소드를 가진다고 선언하는 것이다. 실질적인 구현은 이를 구현한다고 선언하는 클래스에 맡긴다.

interface Shape {
    getArea(): number;
}

class React implements Shape {
    width : number;
    height : number;

    constructor(width, height){
        this.width = width;
          this.height = height;
    }
}
// error: Class 'Rect' incorrectly implements interface 'Shape'. Property 'getArea' is missing in type 'Rect'.

위 코드에서는 React라는 클래스가 implements 키워드를 통하여 Shape라는 인터페이스를 구현할 것이라고 선언하는 것이다.
일단 인터페이스를 구현하기로 했으면, 해당 인터페이스에 있는 프로퍼티 및 메소드를 전부 가지거나 구현해야 한다.
여기에서는 getArea라는 메소드를 구현하지 않아서 에러가 발생한다.

Optional 프로퍼티

인터페이스의 모든 프로퍼티 및 메소드는 구현하는 클래스에서 기본적으로 가지고 있어야될 것 들 이지만, Optional proterty는 말 그대로 선택적으로 구현하는 프로퍼티이다. 프로퍼티 식별자 뒤에 간단하게 ?를 붙여서 사용한다.

interface Shape{
  width? : number;
  height? : number;
  radius? : number;
  getArea() : number;
}

class React implements Shape{
  width : number;
  height : number;

  constructor(width, height){
    this.width = width;
    this.height = height;
  }

  getArea() : number{
      return this.width * this.height;
  }
}

class Circle implements Shape {
  radius : number;

  constructor(radius){
     this.radius = radius; 
  }

  getArea(): number {
    return Math.PI * this.radius * this.radius;
  }
}

인터페이스를 구현하는 클래스에서는 Optional프로퍼티를 가지고 있지 않더라도 에러가 발생하지 않는다.

Indexable

Js의 객체는 프로퍼티 접근자(Property accessor)를 두가지 제공한다. 하나는 점 표기법(Dot notation)이고, 다른 하나는 괄호 표기법(Bracket notation)이다. 기본적으로 점 표기법을 자주 사용하기는 하지만, 동적으로 프로퍼티에 접근하려는 경우 문자열으로 프로퍼티에 접근 할 수 있는 괄호 표기법을 사용하기도 한다.


const dict = {
  foo: 1,
  bar: 2
};

Object.keys(dict)
.forEach(k => console.log(dict[k]));

위 코드를 js로 접근하면 정상적으로 동작한다.
그러나 Typescript에서는 dict의 프로퍼티를 동적으로 접근하는부분에서 error : Index signature of object type implicitly has an 'any'type이라는 에러가 나면서 컴파일 되지 않는다.
TypeScript가 괄호 표기법을 제공하지 않는 것은 아니지만, 동적인 키 값을 사용하게 되면 에러가 발생한다.

에러 원인은 간단하다. 어던 타입의 프로퍼티에 접근하는 지 알 수 없기 때문에 리턴 값을 묵시적으로 any타입으로 변환하여 에러를 띄우는 것이다. 이를 해결하기 위해서는 noImplicitAny값을 false로 바꾸던지, 객체 자체를 Indexable하게 바꾸는 방법밖에는 없다.

interface Indexable {
  [key : string] : any;
}

const dict : Indexable = {
  foo :1,
  bar : 2
};

Object.keys(dict).forEach(k => console.log(dick[k]);

함수 인터페이스

Typescript의 인터페이스는 함수의 인터페히스를 정의할 수도 있다.

interface numberOperation {
  (arg1: number, arg2: number): number;
}

const sum: numberOperation = (arg1: number, arg2: number): number => {
  return arg1 + arg2;
};

const multiply: numberOperation = (arg1, arg2) => {
  return arg1 * arg2;
};

const toArray: numberOperation = (arg1: any, arg2: any): any[] => { // error: Type '(arg1: any, arg2: any) => any[]' is not assignable to type 'numberOperation'. Type 'any[]' is not assignable to type 'number'.
  return [arg1, arg2];
};

문법은 식별자 없이, 받아야할 인자의 타입과, 리턴 타입만을 표기하면 된다. 쉽게 예상할 수 있겠지만, 이 인터페이스를 구현하는 함수는 반드시 정의했던 타입의 인자를 받아 정의했던 타입을 리턴해야만 에러없이 컴파일이 된다.

multiply처럼, 정의했던 인턴페이스대로 구현된 함수는 굳이 타입을 명시할 필요는 없다.이상한 타입만 명시하지 않으면 된다. 여기서 이상한 타입이라 함은 any와 애초에 인터페이스에서 선언했떤 number를 제외한 타입들을 말한다.
타입을 명시하지 않으면 함수를 실제로 사용할 때 인자로 이상한 타입을 넘겨도 될 것 같지만, 그렇게 하면 타입이 맞지 않아 에러가 뜬다.

생성자 인터페이스

class Dog {
  constructor(name, age) {
    this.name = name;
    this.age  = age;
  }
}

class Cat {
  constructor(name, age) {
    this.name = name;
    this.age  = age;
  }
}

function createAnimal(cstr, name, age) {
  return new cstr(name, age);
}

console.log(createAnimal(Dog, '팔랑', 15));
console.log(createAnimal(Cat, '쭈쭈', 10));

다음과 같은 코드는 js에서는 작동하지만,Typescript에서 구현하기가 간단하지 않다.


class Dog {
  name: string;
  age: number;

  constructor(name: string, age: number) {
    this.name = name;
    this.age  = age;
  }
}

class Cat {
  name: string;
  age: number;

  constructor(name: string, age: number) {
    this.name = name;
    this.age  = age;
  }
}

function createAnimal(cstr: Function, name: string, age: number) {
  return new cstr(name, age); // error: Cannot use 'new' with an expression whose type lacks a call or construct signature.
}

createAnimal(Dog, '팔랑', 15);
createAnimal(Cat, '쭈쭈', 10);

위 코드는 컴파일이 안된다. Typescript에서는 new와 함께 일반 함수를 호출할 수 없기 때문이다.
따라서 TypeScript가 생성자로 인식할 만한 어떤 타입을 써주어야 한다. 이 때 new라는 키워드를 이용해서 생성자의 인터페이스를 정의 할 수 있다.

interface Animal {
  name: string;
  age : number;
}

interface AnimalConstructor {
  new (name: string, age: number): Animal;
}

class Dog implements Animal {
  name: string;
  age : number;

  constructor(name: string, age: number) {
    this.name = name;
    this.age  = age;
  }
}

class Cat implements Animal {
  name: string;
  age : number;

  constructor(name: string, age: number) {
    this.name = name;
    this.age  = age;
  }
}

function createAnimal(cstr: AnimalConstructor, name: string, age: number) {
  return new cstr(name, age); // OK
}

createAnimal(Dog, '팔랑', 15);
createAnimal(Cat, '쭈쭈', 10);

생성자는 인스턴스와 다른 타입이다.
이 코드에서 객체 인스턴스가 Animal타입, 그리고 생성자는 animalConstrucotr타입이다.AnimalConstructornew라는 키워드와 함께 함수 인터페이스 문법을 사용하는데 이것이 TypeScript에서 생성자를 정의하는 문법이다. 앞에 new 키워드가 들어간다는 걸 제외하면, 일반 함수 인터페이스 문법과 다른 점이 없다. 다만 Animal 타입을 리턴한다는 것을 명시적으로 선언해줘야 한다.


참조

TypeScript : 인터페이스(Interface)

[Interface - poiemaweb.com](https://poiemaweb.com/typescript-interface)

반응형