[Day 39] TypeScript 기본 문법 (2)

송히·2023년 11월 10일
post-thumbnail

Today I Learn📖

  • 인터페이스 개요 (강의)
  • 인터페이스의 함수, 클래스 (강의)
  • 인터페이스의 인덱싱 가능 타입 (강의)
  • 인터페이스 확장 (강의)
  • 함수의 명시적 this 타입 (강의)
  • 함수 오버로딩 (강의)
  • 클래스의 접근 제어자 (강의)
  • 추상 클래스 (강의)
  • 데코레이터 (강의)

인터페이스 (Interface)

: 개체(객체, 배열, 함수, 클래스 등)를 정의하는 타입 (유니온 정의할 땐 잘 사용 안 함)
=> Interface 인터페이스명 {}으로 사용
-> type 별칭과 똑같은 역할
=> 인터페이스, type 별칭 둘 다 ?, readonly 옵션 사용 가능

interface UserI {
  name: string
  readonly age: number // readonly 붙으면 읽기 전용
  isValid?: boolean // 선택 옵션
}
const user: UserI = {
  name: 'Heropy',
  age: 85
  // isValid 없어도 에러 안 남
}
user.age = 22 // 에러 발생 -> 읽기 전용이라 수정 안 됨

인터페이스의 함수, 클래스

함수

: 호출 시그니처(Call signature) 사용
=> (매개변수: 타입): 반환 타입으로 사용
-> 타입의 함수 타입이랑 같은 역할

interface User {
  name: string
  age?: number
}
interface GetUserNameI {
  (u: User) : string // 호출 시그니처
}
const getUserName: GetUserNameI = (user: User) = user.name // 다른 곳에서 반환 타입으로 사용 가능

클래스

: 구문 시그니처 (Construct signature) 사용
=> new (매개변수: 타입): 반환타입으로 사용

interface UserI {
  name: string
  getName(): string // 메서드도 정의 가능
}
interface UserC { 
  new (n: string): UserI // 구문 시그니처
  // 클래스가 실행된 결과는 인스턴스 -> 인스턴스의 타입을 반환값으로 써줘야함
}
class User implements UserI { // TS에서는 `class 클래스명 implements 타입 {}`으로 클래스 사용, implements 뒤에 여러 개 작성 가능 (쉼표로 구분)
  public name
  constructor (name: string) {
    this.name = name
  }
  getName() {
    return this.name
  }
}
const user = new User('Heropy') // 생성자 함수를 이용해 인스턴스 생성
user.getName () // 'Heropy'

function hello(userClass: UserC, msg: string) { // 클래스 자체를 매개변수로 받을 때는 구문 시그니처 생성해서 반환값으로 써야함
  const user = new userClass('Heropy")
  return `Hello ${user.getName()}, ${msg}`
}
hello(User, 'good morning!')

인터페이스의 인덱싱 가능한 타입

: 인덱스 시그니처 사용
=> [매개변수: 타입]: 반환 타입으로 사용
-> 대괄호 속 key를 통해 value에 접근할 수 있음, key는 아직 정해져있지 않은 이름

  • 인덱스 시그니처가 없다는 에러를 만나면, 에러 나는 객체에 인덱스 시그니처 추가하면 됨 !
// 인터페이스로 배열 타입 정의 가능 -> 인덱스 시그니처 정의하면 인덱싱도 가능
interface Arr { 
  [key: number]: string // 인덱스 시그니처
}
const arr: Arr = ['A', 'B', 'C']
console.log (arr[1]) // 'B'

// 인터페이스로 객체 타입 정의 가능 -> 인덱스 시그니처 정의하면 인덱싱도 가능
interface Obj { 
  [key: string]: number // 인덱스 시그니처
}
const obj: Obj = { a: 123, b: 456, c: 789 }
console. log (obj ['b']) // 456
console. log(obj.b) // 456

// 객체에 속성 추가 가능
interface User {
  [key: string]: string | number // 인덱스 시그니처, 선택 옵션이라는 의미, key가 string이고 값이 string 또는 number인 속성은 다 추가 가능
  name: string
  age: number
}
const user: User = {
  name: 'Heropy',
  age: 85,
  email: 'thesecon@gmail.com', // 추가된 속성
  isValid: true // bool은 반환 타입에 없기 때문에 에러 발생
}


  • 타입 인덱싱: 인터페이스가 만든 타입 속 속성의 타입도 꺼내서 사용 가능한 문법

인터페이스 확장

  1. 인터페이스명 extends 타입 으로 사용
interface UserA {
  name: string
  age: number
}
interface UserB extends UserA { // userA 기반으로 확장
  isValid: boolean
}
const user: UserB = {
  name: 'Heropy',
  age: 85,
  isValid: true
}
  1. 중복된 인터페이스명 사용 => 중복된 내용들은 병합됨
interface FullName {
  firstName: string
  lastName: string
}
interface FullName {
  middleName: string
  lastName: number // 에러 발생 -> 앞에 선언된 변수와 이름 같으면 타입까지 같아야함
}
const fullName: FullName = {
  firstName: 'Tomas',
  middleName: 'Sean',
  lastName: 'Connery'
}

함수의 명시적 this 타입

: this의 타입을 인터페이스를 통해 미리 알려주는 것

interface User {
  name: string
}
function greet(this: User, msg: string) { // 명시적 this 타입 사용됨
  return `Hello ${this.name}, ${msg}`
}
const heropy = {
  name: 'Heropy'
  greet
}
heropy.greet('Good morning~') // 'Hello Heropy, Good morning~'
const neo = {
  name: 'Neo'
}
greet.call(neo, 'Have a great day!') // 'Hello Neo, Good morning~'

함수 오버로딩

: 한 함수가 n가지 방법으로 호출될 수 있을 때 사용, 유니온 타입은 의도와 다르게 동작 가능하기 때문에 함수 오버로딩 사용함
=> 함수 선언부 -> 함수 구현부로 사용

// 함수 오버로딩
function add(x: string, y: string): string // 선언부 1
function add(x: number, y: number): number // 선언부 2
function add(x: any, y: any): any { // 구현부
  console.log(x, y)
  return x + y
}

add('Hello', 'World') // 선언부 1에 해당
add(12, 34) // 선언부 2에 해당
add('Hello', 34) // 에러 발생 -> 타입이 안 맞음

// 인터페이스에서의 함수 오버로딩
interface UserBase {
  name: string
  age: number
}

interface User extends UserBase { // 함수 오버로딩 사용됨
  updateInfo(newUser: UserBase): User // updateInfo 형식 1
  updateInfo(name: string, age: number): User // updateInfo 형식 2
}

const user: User = {
  name: 'Heropy',
  age: 85,
  updateInfo: function(nameOrUser: UserBase | string, age?: number) { // 객체를 받거나 string & number를 각각 받기
    if (typeof nameOrUser === 'string' && age !== undefined) {
      this.name = nameOrUser
      this.age = age
    } else if (typeof nameOrUser === 'object') {
      this.name = nameOrUser.name
      this.age = nameOrUser.age
    }
    return this
  }
}

클래스의 접근 제어자

접근 제어자

=> public (생략 가능), protected, private

class Animal {
  private name: string // Animal 클래스 외 다른 곳에서는 접근 불가
  protected sound: string // Animal 클래스 + Animal 클래스를 확장한 곳에서만 사용 가능

  constructor(name: string, sound: string) {
    this.name = name
    this.sound = sound
  }
}

class Dog extends Animal {
  public color: string // public은 굳이 안 써도 됨

  constructor(name: string, sound: string, color: string) {
    super(name, sound)
    this.color = color
  }

  private speak() {
    console.log(this.name)  // 에러 발생 -> private이라 접근 불가
    return this.sound // Animal 클래스  확장한 곳이니까 접근 가능
  }

  getColor() {
    return this.color
  }
}

const dog = new Dog('흰둥이', '멍멍!', 'white')

dog.speak()         // 에러 발생 -> private이라 접근 불가
dog.getColor()      // 'white'
dog.name            // 에러 발생 -> private이라 접근 불가
dog.sound           // 에러 발생 -> protected 접근 불가
dog.color           // 'white'

수식어

=> 접근제어자 뒤에 작성
-> static, readonly 등이 있음

class Animal {
  private name: string
  public readonly sound: string // readonly니까 읽기 전용

  constructor(name: string, sound: string) {
    this.name = name
    this.sound = sound
  }

  static speak() { // static이니까 인스턴스에서는 접근 불가 (클래스 자체를 호출해서 사용)
    console.log('Animal speak!')
  }
}

class Dog extends Animal {
  public color: string

  constructor(name: string, sound: string, color: string) {
    super(name, sound)
    this.color = color
    this.sound = '야옹!' // 읽기 전용이라 수정 불가
  }

  getColor() {
    return this.color
  }
}

const dog = new Dog('흰둥이', '멍멍!', 'white')

Animal.speak() // speak가 static 메서드니까 클래스 이름으로 직접 호출
Dog.speak() // Dog도 Animal을 상속받았으니 가능
dog.speak() // 에러 발생 -> 인스턴스에서는 호출 불가


// 생성자 매개변수 속성 축약 -> 위 코드의 class Animal과 동일
class Animal {
  constructor( // 매개변수와 속성의 이름이 같으면 constructor로 넘길 때 축약 가능
    private name: string,
    public readonly sound: string
  ) {}

  static speak() {
    console.log('Animal speak!')
  }
}

추상 클래스

: 클래스의 구조를 강제하기 위한 것, 직접 인스턴스를 만들 수는 없고 상속을 통해서만 사용 가능
=> 클래스와 메서드에 abstract붙임
-> abstract로 타입만 선언 하거나 메서드 직접 구현도 가능

abstract class AnimalA {
  abstract color: string // 추상 속성 -> 자식이 반드시 구현해야함
  abstract getColor(): string // 추상 메서드 -> 자식이 반드시 구현해야함

  constructor(public sound: string) {} // 모든 자식이 공통으로 갖는 내용이니까 직접 지정
  speak() { // 직접 구현한 일반 메서드 -> 모든 자식이 공통으로 갖는 내용이니까
    return this.sound
  }
}

class Dog extends AnimalA {
  constructor(
    sound: string,
    public color: string // public을 붙이면 자동 속성 생성 + 할당됨 (안 붙이면 그냥 매개변수)
  ) {
    super(sound) // 상속 받았으니 super 호출 필요
  }

  getColor() { // 자식이 추상 메서드 구현함
    return this.color
  }
}

const dog = new Dog('멍멍!', 'white')
dog.speak()     // '멍멍!'
dog.getColor()  // 'white'

추상클래스와 인터페이스의 차이

특징추상클래스인터페이스
사용법abstract class 클래스명 {}interface 인터페이스명 {}
상속extends (1개)implements (여러, 개, 가능)
메서드 구현가능 (구현부 포함)불가 (타입만 정의)
부모 호출super() 필수상속 아니니까 필요없음
상황부모 클래스의 공통 속성을 규칙으로 만들고 싶을 때타입만 정하고 구현은 자유롭게 할 때
// 추상클래스와 인터페이스 동시 사용 가능
interface AnimalI1 { ... }
interface AnimalI2 { ... }
abstract class CatA { ... }

class Cat extends CatA implements AnimalI1, AnimalI2 { // 동시 사용 가능, extends가 implements보다 먼저 와야함
  constructor(
  ) {
    super() // 추상클래스로 extends 했으니까 super() 호출 필요
  }

데코레이터

: 클래스나 클래스 속 메서드, 속성 등에 기능을 확장하거나 변경, 제어하기 위한 문법
=>tsconfig.json의 "compilerOptions":에 "experimentalDecorators": true 추가 후 사용
-> 사용 위치: 클래스, 클래스의 속성, 클래스의 메서드, 메서드의 매개변수
-> 사용 위치 앞 또는 위에 @데코레이터명을 붙임

// deco.ts
export function deco(target: any) {
  if (target.name !== 'User') { // 생성자 함수로 클래스 선언될 때 단 1번만 실행됨
    throw new Error('클래스의 이름이 User가 아닙니다!')
  }
  console.log('정상적인 클래스의 이름입니다!')

  return class extends target { // return 속 내용은 생성된 클래스가 사용될 때마다 실행됨, 원본 클래스 복사 + 새로운 내용
    constructor(...args: any[]) { // 클래스 속 데이터가 어떤 것일지 모르니까 ...args로 전부 받기
      super(...args) // 전부 받은 걸로 부모 원본 클래스 호출
      fetch('서버주소', {
        //
      })
      console.log('완료!')
    }
  } as any
}

// main.ts
import { deco } from './deco'

@deco // deco 실행, 클래스에 적용할 때는 클래스명 위에 붙임
class User {
  constructor(public name: string) {}

  hello(msg: string) {
    return `Hello ${this.name}, ${msg}`
  }
}

const heropy = new User('Heropy') // deco 속 return 뒷부분 실행
const neo = new User('Neo') // deco 속 return 뒷부분 실행

console.log(heropy)
console.log(neo)

// deco에서 타입 지정할 때 제네릭 사용하는 방법
export function deco<T extends { new (...args: any[]): any }>(target: T): any {
  ...
} // as any는 삭제


😊오늘의 느낀점😊

인터페이스를 공부하며 자바스크립트의 클래스도 다시 한번 복습했다. 또, 타입 작성하는 건 많이 안 것 같은데 함수 오버로딩, 오바라이딩, 수식어 등 새롭게 배우는 개념들이 있어서 이해하는데 시간이 좀 더 걸렸다 🥹
타입으로 선언하는 것과 인터페이스 선언의 차이점이 궁금했는데 선언 방식의 차이고 의미는 같다는 걸 알게됐다 !

profile
데브코스 프론트엔드 5기

0개의 댓글