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 타입으로 추론된다.
✅ 타입 추론의 장점
- 코드의 가독성 향상 : 타입 추론을 사용하면 코드의 가독성이 향상된다. 명시적으로 타입을 지정하지 않아도 코드에서 변수의 타입을 알 수 있기 때문이다.
- 개발 생산성 향상 : 타입 추론을 사용하면 코드 작성 시간을 단축할 수 있다. 명시적으로 타입을 지정하지 않아도 TypeScript가 자동으로 타입을 추론하기 때문이다.
- 오류 발견 용이성 : 타입 추론을 사용하면 코드의 오류를 발견하는 것이 쉬워진다. TypeScript는 변수나 함수의 타입을 추론하여 타입 검사를 수행하기 때문이다.
✅ 타입 추론의 단점
- 타입 추론이 잘못될 경우 코드 오류 발생 : 타입 추론은 TS가 자동으로 수행하는 것이기 때문에, 추론이 잘못될 경우 오류가 발생할 수 있다.
- 명시적인 타입 지정이 필요한 경우가 있음 : 타입 추론만으로는 부족한 경우가 있다. 특히, 복잡한 함수나 객체의 경우에는 명시적인 타입 지정이 필요할 수 있다.
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');
- 첫 번째 함수는 printLog 함수에 특정 타입을 주어 작성한 코드로 타입은 명시되었지만, string 타입 외에 다른 타입이 들어온다면 컴파이 에러가 발생한다.
- 이를 해결하기 위해서 2번처럼 중복으로 함수를 선언하는 방법이 있지만 타입의 가독성 및 유지보수성이 나빠진다.
- 또는 3번처럼 | 연산자를 이용해 유니온 타입으로 선언하는 방법이 있다. 이는 들어가는 인수는 해결이 되지만, string과 number가 둘 다 접근할 수 있는 API만 제공한다.
- 제네릭을 사용하면 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'); //에러
- extends 키워들르 사용해 작성하게 되면 타입에 대한 강제는 아니지만 length에 대해 동작하는 인자만 넘겨받을 수 있게 된다.
- keyof 키워드를 사용하면 첫 번째 인자로 받는 객체에 없는 속성들은 접근할 수 없게끔 제한할 수 있다.