배꼽파지 않도록 잘 개발해요

[엘리스sw] 5주차 5일 - 인터페이스, 제네릭 본문

교육/엘리스 SW 트랙

[엘리스sw] 5주차 5일 - 인터페이스, 제네릭

꼽파 2024. 1. 25. 17:54


◆ Interface

◆ Generic


Interface

Interface란

 

Interface
· 일반적으로 변수, 함수, 클래스에 타입 체크를 위해 사용된다.
· 직접 인스턴스를 생성할 수 없고, 모든 메소드가 추상 메소드이다.
· 추상 클래스의 추상 메소드와 달리 abstract 키워드는 사용할 수 없다.
· ES6은 인터페이스를 지원하지 않지만 TypeScript는 인터페이스를 지원한다.

Interface를 사용하는 이유
· 타입의 이름을 짓고 코드 안의 계약을 정의한다.
· 프로젝트 외부에서 사용하는 코드의 계약을 정의한다.
· 다음과 같은 범주에 대해 계약을 정의할 수 있다.

  • 객체의 스펙 (속성과 속성의 타입)
  • 함수의 파라미터
  • 함수의 스펙 (파라미터, 반환 타입 등)
  • 배열과 객체에 접근하는 방식
  • 클래스

Interface 기본 예제

· Interface를 추가하여 함수 매개변수 프로퍼티를 정의할 수 있다.
· 정의한 프로퍼티 값을 누락하거나 정의하지 않는 값을 인수로 전달시 컴파일 에러가 발생한다.

function sayName(obj: { name: string }) {
	console.log(obj.name);
}

let person = { name: "june" };

sayName(person);
interface Person {
	name: string
}

function sayName(obj: person) {
	console.log(obj.name);
}

let person = { name: "june" };

sayName(person);

Person 객체의 구조를 변경하기로 결정한 경우 인터페이스만 업데이트하면 됨.


Properties

Properties
컴파일러는 프로퍼티의 두 가지 요소를 검사한다.

  • 필수요소 프로퍼티의 유무
  • 프로퍼티 타입

아래 예약어로 프로퍼티를 세밀하게 컨트롤 할 수 있다.

  • ? (Optional Properties)
  • readonly (Readonly properties)

 

Optional Properties
· 프로퍼티 선언 시 이름 끝에 ?를 붙여서 표시한다.
· 인터페이스에 속하지 않는 프로퍼티의 사용을 방지하면서, 사용 가능한 프로퍼티를 기술할 때 사용한다.
· 객체 안의 몇 개의 프로퍼티만 채워 함수에 전달하는 "option bags" 같은 패턴에 유용하다.

interface Person {
  name: string;
  age?: number; // 물음표로 옵셔널 프로퍼티임을 표시함
}

function printPerson(person: Person) {
  console.log(`Name: ${person.name}`);
  
  if (person.age !== undefined) {
    console.log(`Age: ${person.age}`);
  }
}

let john: Person = { name: "John" };
let jane: Person = { name: "Jane", age: 25 };

printPerson(john);
// Name: John
printPerson(jane);
// Name: Jane
// Age: 25

 

readonly properties
· 객체가 처음 생성될 때만 값 설정이 가능하고, 이후 수정이 불가능하다.
· 프로퍼티 이름 앞에 readonly를 붙여 사용한다.

interface Car {
  readonly brand: string;  // 수정 불가
  model: string;
}

let myCar: Car = { brand: "Toyota", model: "Camry" };

myCar.brand = "Honda";
// error TS2540: Cannot assign to 'brand' because it is a read-only property.

console.log(`Brand: ${myCar.brand}`)
// Brand: Toyota

 

readonly vs const
· readonly 와 const 의 공통점 : 생성 후에 배열을 변경하지 않음을 보장한다.

· 변수는 const를 사용하고 프로퍼티는 readonly를 사용한다.

 

const 

const numbers: number[] = [1, 2, 3];

numbers.push(4);
console.log(numbers)  
//  [1, 2, 3, 4 ]

numbers = [3, 5, 7];
console.log(numbers)  
// error TS2588: Cannot assign to 'numbers' because it is a constant.

 

readonly

let arr: number[] = [1, 2, 3, 4];

let readonly_arr: ReadonlyArray<number> = arr;

// 가능
arr[0] = 12;
arr.push(5);
arr.length = 100;

// 불가능
readonly_arr[0] = 12;
readonly_arr.push(5);
readonly_arr.length = 100;

Interface types

TypeScript에서 인터페이스는 함수, 클래스에서 사용할 수 있다.

함수
- JavaScript 객체가 가질 수 있는 넓은 범위의 형태를 기술한다.
- 프로퍼티로 객체를 기술하는 것 외에도, 인터페이스는 함수 타입을 설명한다.

클래스
- 클래스가 특정 통신 프로토콜을 충족하도록 명시적으로 강제한다.
- C#과 Java 같은 언어에서 일반적으로 인터페이스를 사용하는 방법과 동일하다.

 

function type
· 함수의 인자의 타입과 반환 값의 타입을 정의한다.
· 함수의 타입을 정의할 때에도 사용한다.

type AddFunction = (num1: number, num2: number) => number;

const add: AddFunction = (num1, num2) => num1 + num2;

const result: number = add(3, 5);
console.log(result); // 출력: 8


class type
· 클래스가 특정 계약(contract)을 충족하도록 명시적으로 강제한다.

interface Shape {
  calculateArea(): number;
}

class Circle implements Shape {
  radius: number;

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

  calculateArea() {
    return Math.PI * this.radius ** 2;
  }
}

const circle: Shape = new Circle(5);
console.log(circle.calculateArea()); // 78.54

Shape 인터페이스는 숫자를 반환하는 calculateArea 메소드를 사용하여 계약을 정의합니다.
Circle 클래스는 필수 calculateArea 메소드를 제공하여 Shape 인터페이스를 구현합니다.
Circle 클래스의 인스턴스가 생성되어 Shape로 사용되며, 이는 `Shape`에 의해 정의된 계약을 준수함을 나타냅니다.


interface 확장
· 클래스와 마찬가지로 인터페이스도 인터페이스 간의 확장이 가능하다.

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

interface Employee extends Person {
  employeeId: string;
  department: string;
}

const employee: Employee = {
  name: 'John Doe',
  age: 30,
  employeeId: '123',
  department: 'IT',
};


hybrid type
· 자바스크립트의 유연하고 동적인 타입 특성에 따라 인터페이스도 여러 가지 타입을 조합할 수 있다.
· 아래 코드와 같이, 함수 타입이면서 객체 타입을 정의할 수 있는 인터페이스도 구현할 수 있다.

interface User {
  name: string;
  age: number;

  greet(): void;
}

const myUser: User = {
  name: 'Alice',
  age: 25,
  greet() {
    console.log(`Hello, my name is ${this.name} and I'm ${this.age} years old.`);
  }
};

myUser.greet();

디자인 패턴 (stratefy pattern)

interface를 활용한 디자인 패턴
객체가 할 수 있는 행위들을 전략(strategy)으로 만들어두고, 동적으로 행위의 수정이 필요한 경우 전략을 바꾸는 것만으로 수정이 가능하도록 만든 패턴


Generic

Generic 개념

· 데이터타입을 일반화하는 것
· 정적 type 언어는 클래스나 함수를 정의할 때 type을 선언해야 한다.
   ex. C언어는 int type 변수를 선언하면 정수형 값만 할당할 수 있다.

· Generic은 코드를 작성할 때가 아니라 코드가 수행될 때 타입을 명시한다.
· 코드를 작성할 때 식별자를 써서 아직 정해지지 않은 타입을 표시한다.
- 일반적으로 식별자는 T, U, V ...를 사용한다.
- 필드 이름의 첫 글자를 사용하기도 한다.

 

Generic을 사용하는 이유

  • 재사용성이 높은 함수와 클래스를 생성할 수 있다.
    - 여러 타입에서 동작이 가능하다. (한번의 선언으로 다양한 타입에 재사용할 수 있다.)
    - 코드의 가독성이 향상된다.
  • 오류를 쉽게 포착할 수 있다.
    any 타입을 사용하면 컴파일 시 타입을 체크하지 않는다.
    - 타입을 체크하지 않아 관련 메소드의 힌트를 사용할 수 없다.
    - 컴파일 시에 컴파일러가 오류를 찾지 못한다.
  • Generic도 any처럼 미리 타입을 지정하지는 않지만, 타입을 체크해 컴파일러가 오류를 찾을 수 있다.

Generic을 사용해 function과 class 만들기

function

function echo<T>(value: T): T {
  return value;
}

const resultString: string = echo("Hello, TypeScript!");
const resultNumber: number = echo(42);

console.log(resultString); // Hello, TypeScript!
console.log(resultNumber); // 42

 

class

class Box<T> {
  constructor(private value: T) {}

  getValue(): T {
    return this.value;
  }

  setValue(newValue: T): void {
    this.value = newValue;
  }
}

const stringBox = new Box("Hello");
const numberBox = new Box(42);

console.log(stringBox.getValue()); // Hello
console.log(numberBox.getValue()); // 42

stringBox.setValue("World");
console.log(stringBox.getValue()); // World

Union type

|를 사용해 두 개 이상의 타입을 선언하는 방식

 

Union과 Generic 모두 여러 타입을 다룰 수 있음.
Union type의 특징

  • 선언한 공통된 메소드만 사용 가능
  • 리턴값이 하나의 타입이 아닌 선언된 Union 타입으로 지정됨.
function printLength(value: string | number): void {
  console.log(value.length); // 에러 length는 숫자에 없음
}
function getStringOrNumber(flag: boolean): string | number {
  return flag ? "Hello" : 42;
}

const result: string | number = getStringOrNumber(true);
console.log(result)  // Hello

 

Union

type StringOrNumber = string | number;

function printStringOrNumber(value: StringOrNumber): void {
  console.log(value);
}

printStringOrNumber("Hello");  // Hello
printStringOrNumber(42);       // 42

 

Generic

type Box<T> = {
  value: T;
};

function printBoxValue<T>(box: Box<T>): void {
  console.log(box.value);
}

const stringBox: Box<string> = { value: "World" };
const numberBox: Box<number> = { value: 123 };

printBoxValue(stringBox);  // World
printBoxValue(numberBox);  // 123

제약조건 (Constraints / keyof)

Constraints
Generic T에 제약 조건(extends)을 설정한다. 
제약조건을 벗어나는 타입을 선언하면 에러가 발생한다.

const printMessage = <T extends string | number>(message: T): T => {
	return message;
}

printMessage<String>("1");
printMessage<Number>(1);
printMessage<Boolean>(false);  // 에러 : T extends의 제약조건에 불린이 없음

 

keyof

매개변수로 주어진 객체의 키의 타입으로 제한하는 타입 매개변수를 선언할 수 있음.

const getProperty = <T extends object, U extends keyof T>(obj: T, key: U) => {
	return obj[key]
}

getProperty({ a : 1, b : 2, c : 3 }, "a");
getProperty({ a : 1, b : 2, c : 3 }, "z");  // 에러 : z는 객체의 프로퍼티 네임이 아님

디자인 패턴 (Factory Pattern with Generics)

· 객체를 생성하는 인터페이스만 미리 정의하고, 인스턴스를 만들 클래스의 결정은 서브 클래스가 내리는 패턴
· 여러 개의 서브 클래스를 가진 슈퍼 클래스가 있을 때, 입력에 따라 하나의 서브 클래스의 인스턴스를 반환한다.

728x90