Typescript -2 문법 (23/04/18)

nazzzo·2023년 4월 19일
0

Typescript 문법



1. 변수와 원시타입

// JS 변수선언
let num = 10
const str = "hello Javascript"

// TS 변수선언
let num2: number = 10
const str2: string = "hello Typescript" 

사실 이렇게 명확한 타입은 굳이 지정해줄 필요가 없습니다
타입스크립트가 대입연산자로 넣은 값을 통해서 타입추론을 할 수 있기 때문
(타입 추론 : 타입이 정해지지 않은 변수에 대해서 컴파일러가 변수의 타입을 스스로 찾아낼 수 있도록 하는 기능)



(↑ 넘버타입에 관한 메서드가 따라오는 것을 확인할 수 있습니다)



↓ 기타 원시타입들

{
    const num:number = 10
    const float:number = 3.14
    const nan:number = NaN
    const infinity:number = Infinity
}


{
    const str: string = "Hello TypeScript"
}

{
    const bool: boolean = true
}

{
    const nullValue: null = null
    const undefinedValue: undefined = undefined
}



함수에 대해서...

함수는 크게 명시적인 반환값의 존재여부에 따라서 타입이 갈립니다
값이 undefined인 함수를 사용할 경우 그 함수의 타입은 void로 지정합니다
(값이 null인 함수는 null타입)
우선 이 void 타입에 대해서만 알아보기로 하겠습니다


void

// void 타입

function print(): void {
    console.log(`hello TypeScript`)
}
print()

const print2 = (): void => {
    console.log(`hello TypeScript`)
}
print2()

함수의 매개변수에도 타입 지정이 필요합니다
(매개변수는 변수와 달리 타입추론이 되지 않기 때문에 꼭 기입하는 습관을 들이기로)

// 전자는 매개변수에 대한 타입지정, 후자는 함수의 반환값에 대한 타입지정
const print = (str: string): void => {
    console.log(`hello ${str}`)
}
print("TypeScript")



↓ 정리

const repository = (): number => {
    return 10
}

const service = (): string => {
    const num = repository()
    return "hello" + num
}

const controller = (): void => {
    try {
        const result = service()    
    } catch (e) {
        console.log(e)
    }
}
controller()

여기서 controller 함수는 명시적인 반환값이 없기 때문에 void 타입입니다


↓ 잘 이해가 안간다면...

const controller = (): void => {
    const result: string = service()    
}
controller()


const controller = (): string => {
    const result = service()
    return result    
}
controller()



+) never: 함수가 무한루프를 돌거나 반환할 수 없는 타입을 지정할 때 사용합니다
하지만 거의 쓰이지 않습니다

never

const throwErr = (): never => {
	throw new Error("에러 발생")
}



2. 참조타입 ~ any vs unknown


{
    // any : 어떤 타입이든 할당할 수 있습니다, 대신 타입추론이 불가능하며 타입 안정성을 보장하지 않습니다
    const a: any = 10
    const b: number = 10
}


{
    // unknown : 어떤 타입이든 할당할 수 있습니다, 타입추론이 가능하며 타입 안정성을 보장합니다
    const getValue = (value: unknown) => {
        return value
    }

    const fn: unknown = getValue("hello world")
}

any 타입을 남발하면 굳이 타입스크립트를 쓸 이유도 사라집니다
그래서 타입을 알 수 없는 값을 다루려면 any 대신 unknown을 사용하는 것이 좋습니다

unknown 타입은 any와 마찬가지로 모든 값을 할당할 수 있습니다
하지만 unknown은 any를 대체하고 타입 안정성을 보장하기 위해 타입스크립트 3.0부터 도입된 타입으로,
값을 할당하기 전에 반드시 타입 검사를 거쳐야 합니다

(이 역시 쓸 일은 잘 없지만, 내부구조를 파악하기 힘든 외부 라이브러리를 가져다 쓸 때 사용할 일이 생기기도 합니다)


위 코드에서 변수 fn의 타입은 미리 지정한 value와 같이 unknown이 됩니다
그러면 함수 fn의 타입을 string으로 지정하려면 어떻게 해야할까요?


{
    const getValue = (value: unknown): string => {
        if(typeof value === "string") return value
        return ""
    }

    const fn: unknown = getValue("hello world")
}

위와 같이 else 상황에 대한 처리까지 끝마쳐야만 타입지정 에러가 발생하지 않습니다



2-2. 참조타입 ~ 배열


참조타입중에서 배열은 배열 안에 있는 요소들의 타입까지 지정해줘야 합니다

const strArr: string[]  = ["1", "2", "3"]
const numArr: number[] = [1, 2, 3]

+) 각 오소의 타입이 다를 때에는

const tuple:[string, number] = ["hello", 123]

↑ 요소의 타입에 따라 따라붙는 메서드들이 다른 것을 확인할 수 있습니다



3. 인터페이스 & 클래스


타입스크립트에서 인터페이스는 코드 설계 단계에서 데이터의 타입을 정의하는 역할을 합니다
주로 객체의 속성들의 타입을 '미리' 정의할 때, 그리고 함수나 클래스를 다룰 때에도 자주 사용됩니다

인터페이스는 추상적이며 선언적인 개념입니다
그래서 인터페이스 코드는 컴파일러가 타입 체크를 수행할 때만 사용되며, 컴파일 후에는 빌드파일에 포함되지 않습니다


예제1

interface Product {
    // 인터페이스는 객체가 아니기 때문에 속성 구분을 위한 쉼표가 필요하지 않습니다
    name: string;
    price: number;
}

const product1:{name: string, price: number} = { name: "macbook", price: 22_000 }
const product2: Product = { name: "iphone", price: 12_000 }
const products: Product[] = [product, product2]

↑ 위와 같이 인터페이스는 객체의 구조를 정의하는 일종의 가이드 역할을 합니다


예제2

interface Board {
    readonly id: number; // 읽기 전용
    readonly writer: string;
    subject: string;
    content: string;
    date: Date;
    hit: number;
  	like?: number // 선택적 속성
}


const board1: Board = { id: 1, writer: "kim", subject: "hello", content: "world", date: new Date("2022-01-01"), hit: 0 }
const board2: Board = { id: 2, writer: "lee", subject: "hello2", content: "world2", date: new Date("2022-01-02"), hit: 2, like: 1 }

console.log(board1)
console.log(board2)

board1.id = 3 // 에러 발생
  • 상황에 따라 속성의 사용유무가 달라진다면 해당 속성을 인터페이스에서 선택적 속성으로 지정해야 합니다
  • 인터페이스에 readonly(읽기전용)를 선언하면 const를 쓸 때처럼 객체 생성 후 속성의 재할당이 불가능해집니다

다음은 class에서의 인터페이스 예제입니다

class Product {
    name: string
    price: number
    constructor(name: string, price:number) {
        this.name = name;
        this.price = price;
    }
}

const product3: Product = new Product("iphone", 12_000)

여기서 클래스의 속성은 클래스 레벨에서 정의되는 것이고, 생성자 함수의 매개변수는 로컬 변수입니다
이들은 서로 다른 변수로 취급되기 때문에 각각의 타입을 별도로 지정해야 합니다

따라서 Product 클래스에서 name과 price 속성의 타입을 선언하고,
생성자 함수에서도 (인스턴스 생성시 사용할) name과 price 매개변수의 타입을 지정해주어야 에러가 발생하지 않습니다



덧붙여서 클래스에도 인터페이스 적용이 가능합니다

class Product2 implements Product {
    name: string
    price: number
    constructor(name: string, price:number) {
        this.name = name;
        this.price = price;
    }
}

const product4: Product2 = new Product2("airpod", 4_000)

인터페이스를 클래스에 적용시킬 때는 implements 키워드를 사용해야 합니다
그리고 인터페이스에 따라 구현된 클래스는 해당 인터페이스의 요구 사항을 충족해야 합니다

이런 식으로 클래스에서 인터페이스를 사용하면 다른 코드에서도 해당 클래스가 구현한 인터페이스를 참조하여 타입 검사를 수행할 수 있습니다
클래스를 만들 때에도 항상 인터페이스를 미리 정의하는 습관을 들이는 것을 추천합니다


그런데 사실 위와 같은 클래스 설계는 올바른 방식이라 볼 수 없습니다



3-2. 클래스 설계


클래스 설계는 생성될 객체의 기능(메서드)에 초점을 맞춰야 합니다


예제1

// 생성될 객체(인스턴스)의 모양을 설계하는 인터페이스
interface UserInfo {
    userid: string;
    username: string;

}

// 클래스의 기능 설계(메서드 정의)는 추상 클래스로
abstract class Person {
  	// 여기서 addUser 함수의 리턴값은 UserInfo 형태를 따라야 합니다
    abstract addUser( userid: string, username: string ): UserInfo;
}


class User extends Person {
  	// 상속받은 추상 클래스를 구체적으로 구현합니다
    addUser (userid: string, username: string): UserInfo {
        return { userid, username }
    }
}



예제2

접근제한자(private 혹은 #) ~ 클래스 설계는 직접적으로 속성값에 접근할 수 없도록 해야 합니다
(메서드를 통해서만 접근할 수 있도록... 객체지향 설계의 캡슐화)

interface IProduct {
    name: string;
    price: number;
}

class Product {
    private name: string
    private price: number
    private amount: number
    private discountRate: number

    constructor(name: string, price: number, amount: number, discountRate: number) {
        this.name = name;
        this.price = price;
        this.amount = amount;
        this.discountRate = discountRate;
    }

    getProduct() {
        return {
            name: this.name,
            price: this.price,
            amount: this.amount,
            discountRate: this.discountRate
        }
    }
    getName(): string {
        return this.name
    }
    getPrice(): number {
        return this.price * (1 - this.discountRate)
    }
    setDiscountRate(rate: number): void {
        this.discountRate = rate
    }
    getToalPrice(): number {
        return this.price * this.amount
    }
}


const product = new Product("apple", 1_000, 20, 0);
const product2 = new Product("banana", 800, 55, 0.1);

console.log(product.getToalPrice())
// 20000
console.log(product2.getName() + " " + product2.getPrice())
// banana 720



두번째 예제를 읽으면서 느껴지는 것은 기능을 추가할 때마다 클래스가 비대해진다는 것입니다
메서드가 추가될수록 클래스의 기능적 구분도 불분명해지고, 클래스를 사용중인 코드의 관리도 어려워집니다
그리고 인스턴스 생성시 기입해야할 인자도 너무 많습니다


이에 대해서는 여러 방편이 있겠지만, 객체지향 설계(OOP)의 방법론 중에는
단일 책임 원칙(Single Responsibility Principle)을 적용하여 클래스를 분리하는 방법이 있습니다
SRP는 클래스는 단 하나의 책임만 가져야 한다는 원칙으로, 이에 따르면 각 클래스는 하나의 기능에만 집중해서 구현해야 합니다

또 하나는 개방 폐쇄 원칙(Open Closed Principle)인데, OCP는 확장에 대해서는 개방적(open)이고,
수정에 대해서는 폐쇄적(closed)이어야 한다는 의미로 정의됩니다

이러한 몇가지 객체지향 설계 원칙을 적용해서 코드의 유지보수성과 확장성을 좀 더 높여봅시다


예제3

// 제품에 대한 기본정보를 설정하는 클래스
class Product {
    private name: string
    private price: number

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

    getName(): string {
        return this.name;
    }

    getPrice(): number {
        return this.price;
    }
}


// 할인 기능을 위한 인터페이스 생성
interface Discount {
  	// 추상 메서드
    getDiscount(price: number): number;
}


// 고정 할인가 적용을 위한 클래스
class DiscountAmount implements Discount {
    private amount: number // 고정 할인액
    constructor(amount: number) {
        this.amount = amount
    }

    getDiscount(price: number): number {
        return price - this.amount   
    }
}

// 할인율 적용을 위한 클래스
class DiscountRate implements Discount {
    private rate: number // 할인율
    constructor(rate: number) {
        this.rate = rate
    }

    getDiscount(price: number): number {
        return price * (1 - this.rate / 100)   
    }
}


// 제품에 할인(최종 가격)을 적용하는 클래스
class ProductDiscount {
    private product: Product
    private discount: Discount
    constructor(product: Product, discount: Discount) {
        this.product = product
        this.discount = discount
    }
    getNewPrice(): number {
        return this.discount.getDiscount(this.product.getPrice())
    }
}


const product1 = new Product('macbook', 10_000);
const product2 = new Product('iphone', 6_000);

const productWithDiscountRate = new DiscountRate(10);
const productWithDiscountAmount = new DiscountAmount(2_000);

const product1WithDiscount = new ProductDiscount(product1, productWithDiscountRate);
const product2WithDiscount = new ProductDiscount(product2, productWithDiscountAmount);


console.log(product1WithDiscount.getNewPrice()) // 9000
console.log(product2WithDiscount.getNewPrice()) // 4000
  • 앞으로의 코드 설계는 if문과 인자값을 최소화하는 방향으로...

0개의 댓글