카테고리 없음

[TypeScript] TypeScript Basic 2 // 25.02.28

taecode 2025. 2. 28. 20:37

TypeScript의 타입 별칭 (Type Aliases)


 - 타입 별칭은 타입의 새로운 이름을 만드는 것으로 새로운 이름으로 기존의 타입을 참조하는 것을 의미한다. 타입 별칭을 이용하여 타입의 새로운 이름을 만들 때 키워드 type 을 사용하여 작성한다.

type MyString = string;

let str1: string = 'hello!';

// string 타입처럼 사용할 수 있습니다.
let str2: MyString = 'hello world!';

 

type Person = {
  id: number;
  name: string;
  email: string;
}

//Commentary 인터페이스에서 Person 타입을 참조하고 있습니다.
interface Commentary {
  id: number;
  content: string;
  user: Person;
}

//객체에서 Commentary 인터페이스를 참조하고 있습니다.
let comment1: Commentary = {
    id: 1,
    content: "뭐예요?",
    user: {
        id: 1,
        name: "김코딩",
        email: "kimcoding@codestates.com",
    },
}

//Commentary 인터페이스 내부에 content 프로퍼티가 존재하기 때문에
//content 프로퍼티를 작성하지 않으면 컴파일 에러가 납니다.
let kimcoding: Commentary = {
    id: 1,
    user: {
        id: 1,
        name: "김코딩",
        email: "kimcoding@codestates.com",
    },
};

//Person 타입 내부에 isDeveloper 프로퍼티가 존재하지 않기 때문에
//isDeveloper 프로퍼티를 작성할 시 컴파일 에러가 납니다.
let kimcoding: Commentary = {
    id: 1,
    content: "뭐예요?",
    user: {
        id: 1,
        name: "김코딩",
        email: "kimcoding@codestates.com",
        isDeveloper: true,
    },
};

: 위 코드처럼 인터페이스나 다른 변수를 정의할 때 타입 별칭으로 정의한 타입을 참조하게 됨으로써 코드를 더 간결하고 가독성 좋게 만들 수 있다. 타입 별칭으로 만들어진 타입을 참조할 시에는 인터페이스와 마찬가지로 내부에 정의된 프로퍼티를 전부 참조해야하고 내부에 정의된 프로퍼티 외에 다른 프로퍼티를 더 작성하게 되면 컴파일 에러가 발생한다.

 

 

✅ 인터페이스 vs 타입 별칭

 - 인터페이스의 역할을 타입 별칭이 수행할 수 있지만 차이점이 존재한다.

 - 타입 별칭을 참조하고 있는 타입은 내부에 어떤 프로퍼티들이 정의되어 있는지 보이는 반면 인터페이스를 참조하고 있는 타입은 내부 프로퍼티가 보이지 않는다. 따라서 타입 별칭으로 정의한 타입으로 작성할 시에는 조금 더 편하게 코드를 작성할 수 있다. 

 

 - 또한 타입 별칭은 말 그대로 타입에 새로운 이름을 부여하는 것 뿐이기 때문에 확장이 되지 않지만 인터페이스는 확장이 가능하다. 유연한 코드 작성을 위해서는 인터페이스로 만들어서 필요할 때마다 확장할 수 있다.

 

 

 

 

TypeScript의 타입 추론

 

 - TypeScript는 정적타입을 지원하는 프로그래밍 언어이며, 정적타입 시스템을 사용하면 코드의 안정성을 높이고 디버깅을 용이하게 할 수 있다. TypeScript는 타입 추론(Type Inference)라는 기능을 통해 코드 작성을 도와준다.

 

✅ 타입 추론(Type Inference)의 기본

 - 타입 추론은 변수나 함수의 타입을 선언하지 않아도 TypeScript가 자동으로 유추하는 기능이다.

let isNumber = 123;

: 이 경우 TS는 isNumber의 타입을 자동으로 Number로 추론한다.

 

✅ 최적 공통 타입 (Best common type)

 - TypeScript는 여러 표현식에서 타입 추론이 발생할 때, 해당 표현식의 타입을 사용하여 "최적 공통 타입"을 계산한다.

let x = [0, 1, null];

: x 의 타입을 추론하려면 각 배열 요소의 타입을 고려해야 한다. 후보로는 number와 null이다. 최적 공통 타입 알고리즘은 각 후보의 타입을 고려하여, 모든 후보의 타입을 포함할 수 있는 타입을 선택한다.

 

✅ 문맥상의 타이핑(Contextual Typing)

 - 또 하나의 방식은 문맥상으로 타입을 결정하는 것이다. 이는 코드의 위치(문맥) 기준으로 일어난다.

function add(a, b) {
  return a + b;
}

: add 함수는 두 개의 매개변수를 받아 더한 값을 반환하지만 매개변수의 타입이 명시되어 있지 않다. 이 경우, TS는 매개변수 a와 b의 타입을 자동으로 추론한다. 만약 두 변수가 모두 Number타입이라면, add함수의 반환 값도 Number 타입으로 추론된다.

 

✅ 타입 추론의 장점

  1. 코드의 가독성 향상 : 타입 추론을 사용하면 코드의 가독성이 향상된다. 명시적으로 타입을 지정하지 않아도 코드에서 변수의 타입을 알 수 있기 때문이다.
  2. 개발 생산성 향상 : 타입 추론을 사용하면 코드 작성 시간을 단축할 수 있다. 명시적으로 타입을 지정하지 않아도 TypeScript가 자동으로 타입을 추론하기 때문이다.
  3. 오류 발견 용이성 : 타입 추론을 사용하면 코드의 오류를 발견하는 것이 쉬워진다. TypeScript는 변수나 함수의 타입을 추론하여 타입 검사를 수행하기 때문이다.

✅ 타입 추론의 단점

  1. 타입 추론이 잘못될 경우 코드 오류 발생 : 타입 추론은 TS가 자동으로 수행하는 것이기 때문에, 추론이 잘못될 경우 오류가 발생할 수 있다.
  2. 명시적인 타입 지정이 필요한 경우가 있음 : 타입 추론만으로는 부족한 경우가 있다. 특히, 복잡한 함수나 객체의 경우에는 명시적인 타입 지정이 필요할 수 있다.

 

 

TypeScript의 Class

 - JS와 TS 모두 객체 지향 프로그래밍을 지원하며 클래스를 사용하여 객체를 생성할 수 있지만 몇 가지 차이점이 있다.

 

 

✅ JavaScript에서의 클래스

 - JS에서 클래스는 ES6 에서 처음 도입되었다. 클래스를 사용하면 객체를 생성하고 객체의 속성과 메서드르 ㄹ정의할 수 있다.

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

  greet() {
    console.log(`안녕하세요, 제 이름은 ${this.name}이고, ${this.age}살 입니다.`);
  }
}

const person = new Person('Alice', 30);
person.greet(); // "안녕하세요, 제 이름은 Alice이고, 30살 입니다."

 

 

✅ TypeScript에서의 클래스

 - JS와 비슷하지만 몇 가지 추가된 기능이 있다. 

 * 클래스의 속성과 메서드에 대한 타입을 명시할 수 있다.

class Person {
  name: string;
  age: number;

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

  greet(): void {
    console.log(`안녕하세요, 제 이름은 ${this.name}이고, ${this.age}살 입니다.`);
  }
}

const person = new Person('Alice', 30);
person.greet(); // "안녕하세요, 제 이름은 Alice이고, 30살 입니다."

: 위 코드에서 속성은 문자열과 숫자 타입으로 정의되어 있고, TypeScript에서 클래스를 정의할 때, constructor를 이용하여 초기화하는 멤버들은 전부 상단에서 정의를 해줘야 한다는 것과 constructor 내 인자로 받을 때도 정확히 타입을 명시해 줘야 한다.

 

 

* 클래스와 상속(Inheritance)

 - 인터페이스와 마찬가지로 기존에 존재하던 클래스를 상속받아 확장하여 새로운 클래스를 만들 수 있다. 이때도 extends 키워드를 사용하여 상속할 수 있다.

class Animal {
    move(distanceInMeters: number): void {
        console.log(`${distanceInMeters}m 이동했습니다.`);
    }
}

class Dog extends Animal {
    speak(): void {
        console.log("멍멍!");
    }
}

const dog = new Dog();
dog.move(10);
dog.speak();

: Dog 클래스는 Animal 클래스로부터 프로퍼티와 메서드를 상속받으며, Dog 클래스는 파생 클래스라고도 불리며, 하위클래스라고도 불린다. 여기서 Animal은 기초 클래서, 상위클래스 라고 불린다.

 

 

* public, private 키워드

 - 기본적으로 클래스 내에 선언된 멤버는 외부로 공개되는 것이 디폴트 값이지만 공개된다고 명시적으로도 표시해 줄 수 있다. 이때 public 키워드를 사용하고 드러내지 않을 경우에는 private 키워드로 명시해준다.

class Person {
  public name: string;
  private age: number;

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

  greet(): void {
    console.log(`안녕하세요, 제 이름은 ${this.name}이고, ${this.age}살 입니다.`);
  }
}

 

 

 

* readonly 키워드

 - readonly 키워드를 사용하여 프로퍼티를 읽기 전용으로 만들 수 있다. 읽기 전용 프로퍼티들은 선언 또는 생성자에서 초기화해야 한다.

class Mydog {
    readonly name: string;
    constructor(theName: string) {
        this.name = theName;
    }
}
let spooky = new Mydog("스푸키");
spooky.name = "멋진 스푸키"; // 에러

: 위 코드는 name 이 readonly 로 명시되어 있기 때문에, 값을 변경할 수 없다. 이런 식으로 변경되면 안 될 값을 readonly로 명시하여 보호할 수 있다.

 

 

 

 

TypeScript의 제네릭 (Generic)

- TypeScript의 제네릭은 코드 재사용성을 높이고 타입 안정성을 보장하는 기능이다. 제네릭을 사용하면 함수나 클래스를 작성할 때, 사용될 데이터의 타입을 미리 지정하지 않고, 이후에 함수나 클래스를 호출할 때 인자로 전달된 데이터의 타입에 따라 자동으로 타입을 추론하게 된다. 

 

✅ 제네릭의 필요성

// 1
function printLog(text: string): string {
	return text;
}

printLog('hello'); // 정상
printLog(123); //에러

// 2
function printLog(text: string): string {
	return text;
}

function printLogNumber(text: number): number {
	return text;
}

printLog('hello'); // 정상
printLogNumber(123); //정상

// 3
function printLog(text: string | number) {
	return text;
}

printLog('hello'); // 정상
printLogNumber(123); //정상

// 제네릭 사용
function printLog<T>(text: T): T {
	return text;
}

const str = printLog<string>('hello');

const str: string = printLog<string>('hello');
// 타입 추론 기능
const str = printLog('hello');

 

  1. 첫 번째 함수는 printLog 함수에 특정 타입을 주어 작성한 코드로 타입은 명시되었지만, string 타입 외에 다른 타입이 들어온다면 컴파이 에러가 발생한다.
  2. 이를 해결하기 위해서 2번처럼 중복으로 함수를 선언하는 방법이 있지만 타입의 가독성 및 유지보수성이 나빠진다.
  3. 또는 3번처럼 | 연산자를 이용해 유니온 타입으로 선언하는 방법이 있다. 이는 들어가는 인수는 해결이 되지만, string과 number가 둘 다 접근할 수 있는 API만 제공한다.
  4. 제네릭을 사용하면 printLog함수는 타입을 불문하고 동작하며, any를 쓰는 것과 다르게 인수와 반환 타입에 string을 사용한 1번 함수만큼 정확하다.

 

✅ 인터페이스와 제네릭

 - 인터페이스에도 제네릭을 사용할 수 있다.

interface Item<T> {
	name: T;
	stock: number;
	selected: boolean;
}

const obj: Item<string> = {
	name: "T-shirts",
	stock: 2,
	selected: false
};

const obj: Item<number> = {
	name: 2044512,
	stock: 2,
	selected: false
};

: 이와 같이 작성하면 Item 인터페이스를 사용하여 만든 객체는 name 의 값으로 어떤 타입이 들어갈지만 작성을 해주면 인터페이스를 여러 개 만들지 않고도 재사용을 할 수 있게 된다.

 

 

✅ 클래스와 제네릭

 - 제네릭을 사용하는 TypeScript에서 팩토리를 생성할 때 생성자 함수로 클래스 타입을 참조해야 한다.

class GenericNumber<T> {
    zeroValue: T;
    add: (x: T, y: T) => T;
}

let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function(x, y) { return x + y; };

 

 

✅ 제네릭 타입 변수

 - 제네릭을 사용하기 시작하면, printLog와 같은 제네릭 함수를 만들 때, 컴파일러가 함수 본문에 제네릭 타입화된 매개변수를 쓰도록 강요한다.

function printLog<T>(text: T): T {
	console.log(text.length);
	return text;
}



function printLog<T>(text: T[]): T[] {
	console.log(text.length);
	return text;
}

: 처음과 같이 작성하게 되면 컴파일 에러가 발생한다. 개발자가 string 타입이 아닌 number 타입을 보낼 수도 있기 때문에, T 에는 .length 가 있다는 것을 추론할 수 없기 때문이다.

- 이때는 제네릭에 타입을 줘서 유연하게 함수의 타입을 정의할 수 있다. 두 번째의 함수는 T 라는 변수 타입을 받고, 인자 값으로는 배열 형태의 T 를 받는다. 따라서 제네릭타입이 배열이기 때문에 .length를 허용하게 된다.

 

 

 

✅ 제네릭 제약 조건

 - 제네릭 타입 변수 외에도 제네릭 함수에 어느 정도 어떤 타입이 들어올 것인지 힌트를 줄 수 있다.

// extends 사용
interface TextLength {
	length: number;
}

function printLog<T extends TextLength>(text: T): T {
	console.log(text.length);
	return text;
}


// keyof 사용
interface Item<T> {
	name: T;
	stock: number;
	selected: boolean;
}

function printLog<T extends keyof Item>(text: T): T {
	return text;
}

printLog('name'); //정상
pirntLog('key'); //에러
  1. extends 키워들르 사용해 작성하게 되면 타입에 대한 강제는 아니지만 length에 대해 동작하는 인자만 넘겨받을 수 있게 된다.
  2. keyof 키워드를 사용하면 첫 번째 인자로 받는 객체에 없는 속성들은 접근할 수 없게끔 제한할 수 있다.