
const name: string = '덕배';
// 타입 별칭 사용
type MyName = string;
const name: Myname = '덕배';
타입으로 사용한다는 것에 있어, 타입 별칭은 인터페이스와 유사하다.
인터페이스와 타입 별칭을 비교해보자!
// 인터페이스
interface Me {
name: string,
age?: number
}
// 빈 객체를 Me 타입으로 지정
const me = {} as Me;
person.name = '덕배';
person.age = 30;
person.address = 'Seoul'; // Error
// 타입 별칭
type Me = {
name: string,
age?: number
}
// 빈 객체를 Me 타입으로 지정
const me = {} as Me;
person.name = '덕배';
person.age = 30;
person.address = 'Seoul'; // Error
2개의 가장 큰 차이점은 타입의 확장 가능 / 불가능 여부다.
다시 말해, 인터페이스는 extends 또는 implements될 수 있지만, 타입 앨리어스는 extends 또는 implements될 수 없다. 상속을 통해 확장이 필효다면 인터페이스가 유리하다.
그러면 언제 타입 별칭을 사용할까? 그건 바로 인터페이스로 표혈할 수 없거나 유니온 또는 튜플 등을 사용해야 한다면 타입 별칭을 사용하는 게 유리하다.
// 문자열 리터럴로 타입 지정
type Str = '덕배';
// 유니온 타입으로 타입 지정
type Union = string | null;
// 문자열 유니온 타입으로 타입 지정
type Name = '덕배' | '덕수';
// 숫자 리터럴 유니온 타입으로 타입 지정
type Num = 1 | 2 | 3 | 4 | 5;
// 객체 리터럴 유니온 타입으로 타입 지정
type Obj = {a: 1} | {b: 2};
// 함수 유니온 타입으로 타입 지정
type Func = (() => string) | (() => void);
// 인터페이스 유니온 타입으로 타입 지정
type Shape = Square | Rectangle | Circle;
// 튜플로 타입 지정
type Tuple = [string, boolean];
const t: Tuple = ['', '']; // Error
유니온 타입은 자바스크립트의 OR 연산자 || 와 같이 "A 이거나 B 이다" 라는 의미의 타입이다.
타입스크립트에서는 | 연산자를 사용해 둘 이상의 타입 중 하나를 나타내며, 타입을 여러 개 연결하는 방식으로 사용된다.
function unFunc(username: string | number) {
// ...
}
// 혹은
type Username = string | number;
const me: Username = '덕배'; // 유효
const me2: Username = 1980; // 유효
논리적으로 보면 유니온 타입은 OR, 인터섹션 타입은 AND라고 생각하겠지만, 인터페이스와 같은 타입을 다룰 때는 논리적 사고를 주의해야 한다.
아래 예제를 보면, all 함수의 파라미터 me 타입을 Teacher와 Student 인터페이스의 유니온 타입으로 정의했지만, 이렇게 되면 파라미터의 타입이 Teacher도 되고 Student도 될 수 있다.
함수 내부에서는 name 속성에는 정상적으로 접근할 수 있지만, age와 class 속성에는 접근할 수 없다. 왜? all 함수가 호출되는 시점에서 me의 구체적인 타입이 런타임에 확정되지 않기 때문이다.
결론은, all 함수 안에서는 별도의 타입 가드를 이용해 타입의 범위를 좁히지 않으면 기본적으로 Teacher와 Student 두 타입은 공통적으로 들어있는 속성인 name만 접근할 수 있게 된다.
interface Teacher {
name: string;
age: number;
}
interface Student {
name: string;
class: number;
}
function all(me: Teacher | Student) {
me.name; // 정상 작동
me.age; // 타입 오류
me.class; // 타입 오류
}
const one: Teacher = { name: '덕배', age: 30 };
all(one); // all 함수 안에서 me.class 속성을 접근하고 있으면 함수에서 에러 발생
const two: Student = { name: '덕수', class: 3 };
all(two); // all 함수 안에서 me.age 속성을 접근하고 있으면 함수에서 에러 발생
인터섹션 타입은 & 연산자를 이용해 여러 개의 타입 정의를 하나로 합치는 방식을 의미한다.
예제를 보면, Teacher 인터페이스의 타입 정의와 Student 인터페이스 타입 정의를 & 연산자를 이용해 합친 후, All이라는 타입에 할당한 것이다.
interface Teacher {
name: string;
age: number;
}
interface Student {
name: string;
class: number;
}
type All = Teacher & Student;
// All 타입은 아래와 같이 정의됨
{
name: string;
age: number;
class: number;
}
제네릭은 타입을 마치 함수의 파라미터처럼 사용하는 것을 의미한다.
제네릭은 선언 시점이 아니라 생성 시점에 타입을 명시해 하나의 타입만이 아닌 다양한 타입을 사용할 수 있도록 하는 방식이다. 한번의 선언으로 다양한 타입에 재사용이 가능하다.
T 는 제네릭을 선언할 때 관용적으로 사용되는 식별자로 타입 파라미터(Type parameter)라고 한다.
아래 예제를 보면, all 함수 이름 바로 뒤에 <T> 타입 매개변수를 선언했고, 함수의 인자와 반환 값에 모두 T 라는 타입을 추가했다. 이를 활용해 함수의 인자 타입과 반환 타입을 지정한다.
이렇게 되면 함수를 호출할 때 넘긴 타입에 대해 타입스크립트가 추정할 수 있게 되니까, 함수의 입력값에 대한 타입과 출력 값에 대한 타입이 동일한지 검증할 수 있게 된다.
선언한 함수는 2가지 방법으로 호출할 수 있다.
function all<T>(me: T): T {
return me;
}
// 1. 타입을 명시적으로 지정
let output = all<string>('나는 나야');
// 2. 타입이 추론되어 명시적으로 지정하지 않아도 됨
let outputInferred = all('나는 나야');
console.log(output); // 나는 나야
console.log(outputInferred); // 나는 나야
아래 예제를 보면, 인자를 하나 넘겨 받아 반환해주는 함수인데 첫번째 all 함수의 인자와 반환 값이 모두 string 타입으로 지정되어있고, 두번째 all 함수는 여러 가지 타입을 허용하는 any 타입으로 지정되어 있다.
타입을 바꾼다고 함수의 도작에 문제가 생기는 것은 아니지만, 함수의 인자로 어떤 타입이 들어갔고 어떤 값이 반환되는지 알 수가 없다. 왜? any 타입은 타입 체크를 하지 않기 때문이다.
그래서 위에서 설명한 제네릭을 통해 문제점을 해결할 수 있다.
// 1. 타입 지정
function all(me: string): string {
return me;
}
// 2. 여러가지 타입 허용
function all(me: any): any {
return me;
}
제네릭 인터페이스는 특정한 타입을 정의할 때 미리 지정하지 않고, 나중에 사용할 때 실제 타입을 지정하는 것이다.
제네릭 인터페이스는 객체의 형태를 추상화하고자 할 때 사용되며, T 와 같은 타입 변수를 사용해 주로 여러 타입의 객체가 동일한 구조를 가지되 내부 타입이 다를 수 있는 경우에 활용된다.
아래 예제에서 Car<T>는 T 라는 타입 변수를 가진 제네릭 인터페이스다. numCar 변수는 Car<number>로, strCar 변수는 Car<string>으로 선언되어 각각 value의 타입이 명시된다.
interface Car<T> {
value: T;
}
let numCar: Car<number> = { value: 30 };
let strCar: Car<string> = { value: '자동차' };
제네릭 클래스는 클래스를 정의할 때 특정한 타입을 미리 지정하지 않고, 인스턴스를 생성하거나 메서드를 호출할 때 실제 타입을 지정하는 것이다.
제네릭 클래스는 데이터 구조를 추상화하고자 할 때 사용되며, T 같은 타입 변수를 사용해 클래스 전체에 대한 타입을 추상화한다.
아래 예제에서 Car<T> 클래스는 생성자와 getValue 메소드에서 T 타입을 사용하고 있다. numCar와 strCar는 각각 Car<number>와 Car<string>로 선언되어, 각 인스턴스의 타입이 명시된다.
class Car<T> {
private value: T;
constructor(value: T) {
this.value = value;
}
getValue(): T {
return this.value;
}
}
let numCar = new Car<number>(30);
let strCar = new Car<string>('자동차');
console.log(numCar.getValue()); // 30
console.log(strCar.getValue()); // 자동차
타입스크립트는 기본적으로 변수와 함수, 타입이 전역적으로 사용되기 때문에, 복수의 ts파일이 있을때, 변수와 타입 사용에 있어 서로 충돌하거나 예기치 않게 다른 파일의 변수를 변경하는 등의 문제를 가질 수 있다.
이러한 문제를 해결하는 모듈은 전역적인 범위가 아닌 독립적인 유효 범위 안에서 실행되며, 모듈 안에서 정의된 변수, 함수, 클래스, 인터페이스 등은 외부에서 사용될 수 없는데, export 키워드를 사용해 모듈을 외부로 내보내고, import 키워드를 사용해 다른 모듈에서 내보낸 항목을 가져온다.
이렇게 타입스크립트 모듈은 ES6 모듈을 기반으로 타입스크립트 정적 타입 시스템과 함께 동작한다. 그럼 JavaScript ES6의 모듈과 TypeScript 모듈을 비교해보자!
// math.js
export function add(a, b) {
return a + b;
}
// app.js
import { add } from './math.js';
console.log(add(10,3)); // 출력: 13
// math.ts
export function add(a: number, b: number): number {
return a + b;
}
// app.ts
import { add } from './math';
console.log(add(10,3)); // 출력: 13
기본 디폴트 export는 기본적으로 모듈에서 한 번만 사용할 수 있으며, import 하는 쪽에서 해당 항목에 원하는 이름을 부여할 수 있고, 디폴트 export 된 대상을 바로 사용할 수 있도록 한다.
차이점을 보자면, 디폴트 export, import 는 모듈의 핵심 기능을 내보내고 가져올 수 있고, 일반적인 export, import 는 추가적인 기능이나 유틸리티 함수와 같은 보조적인 항목을 처리하는데 사용된다.
// math.ts
export default function add(a: number, b: number): number {
return a + b;
}
// app.ts
import add from './math';
console.log(add(10,3)); // 출력: 13
// math.ts
export default function add(a: number, b: number): number {
return a + b;
}
// app.ts
import addddd from './math';
console.log(addddd(10,3)); // 출력: 13
타입스크립트 선언 파일 d.ts는 주로 JavaScript 라이브러리나 모듈의 타입 정보를 정의하기 위해 사용되고, 타입 검사를 위한 정보로만 사용된다.
자바스크립트에서 전역 객체인 window 나 document에 대한 타입 정보를 제공하는 Ambient 선언을 만들어 보자!
// global.d.ts
// Window 인터페이스를 확장하여 사용자가 추가한 속성을 정의
interface MyWindow extends Window {
// 사용자가 추가한 속성
myProperty: string;
}
// myWindow 변수를 전역 변수로 사용할 때 해당 타입을 명시적으로 선언
declare var myWindow: MyWindow;
// Document 인터페이스를 확장하여 사용자가 추가한 속성을 정의
interface MyDocument extends Document {
// 사용자가 추가한 속성
myMethod(): void;
}
// myDocument 변수를 전역 변수로 사용할 때 해당 타입을 명시적으로 선언
declare var myDocument: MyDocument;
자바스크립트로 작성된 모듈을 타입스크립트에서 불러오거나, 자바스크립트로만 개발된 패키지를 타입스크립트 프로젝트에서 설치해서 사용할 때 문제점이 있다.
import로 불러오기만 해서는 모듈 안에 있는 것들의 타입을 알 수 없고, 기본적으로 any 타입으로 불러오게 된다. 이럴 때 필요한 게 타입 정의 파일이다.
타입스크립트 파일을 컴파일할 때 --declaration 이라는 옵션과 함께 컴파일하면 자바스크립트 파일을 만들면서 d.ts 파일도 함께 생성된다.
아래 예제에서 me 함수 코드를 명령어로 컴파일해 보면, main.js 파일과 함께 main.d.ts 파일이 생성된다.
// main.ts
export function me(name: string) {
console.log(`${name} 하이!`);
}
# 명령어
tsc main.ts --declaration
// main.d.ts
export declare function me(name: string): void;
자바스크립트도 해보자! 타입스크립트에는 자바스크립트 문법이 포함되어 있기에 자바스크립트 파일도 타입스크립트 컴파일러로 처리할 수 있다.
위 코드와 동일한 자바스크립트 코드를 명령어로 컴파일해 보면, 만들어진 main.d.ts 파일은 추론할 수 없는 타입에 대해 any 타입이라고 적혀있다.
// main.js
export function me(name) {
console.log(`${name} 하이!`);
}
# 명령어
tsc main.js --declaration --allowJS --outDir dist
--allowJS: 자바스크립트 파일도 타입스크립트 컴파일러가 처리하도록 하는 옵션
--outDir: 컴파일 결과물을 지정된 디렉토리에 저장하는 옵션
→ dist라는 폴더로 지정해 준 이유는 main.js의 컴파일 결과는 main.js로 저장되기 때문에,
덮어쓰는 걸 막기 위해서!
그 외에도
--emitDeclarationOnly: .d.ts 파일만 생성하는 옵션
--declarationDir: 선언 파일의 출력 디렉토리를 지정하는 옵션
// main.d.ts
export declare function me(name: any): void;
자바스크립트에서 객체에 동적으로 프로퍼티를 추가하면 타입스크립트에서 타입 안정성을 보장하지 않는다. 그래서 이러한 동적인 프로퍼티 접근을 지원하기 위해 인덱싱(Indexing)을 제공한다.
타입스크립트에서 인덱싱을 사용하면 객체의 프로퍼티에 접근할 때 동적인 키 값을 사용할 수 있으며, 이를 통해 타입 안정성을 유지할 수 있다.
타입스크립트에서 배열 요소와 객체의 속성을 접근할 때는 인터페이스를 사용하면 된다.
일반적으로 배열 요소에 접근할 때는 배열의 인덱스를 사용한다.
아래는 numbers 배열의 첫번째 요소에 접근해 값을 출력한다.
const numbers: number[] = [1,2,3,4,5];
console.log(numbers[0]); // 1
이제 인덱싱을 사용해 보자! StrArray 인터페이스는 배열의 인덱스가 숫자고, 해당 인덱스로 접근했을 때 반환되는 값의 타입은 string임을 정의한다.
따라서 fruits 배열에는 숫자 인덱스로 접근하여 문자열 값을 얻을 수 있다.
interface StrArray {
[index: number]: string;
}
const fruits: StrArray = ['사과', '바나나', '오렌지'];
console.log(fruits[0]); // 사과
key: string 부분은 인덱스 시그니처이며, 이를 통해 객체에는 string 타입의 키로 모든 종류의 값을 추가할 수 있다.
따라서 person 객체에 job이라는 동적인 프로퍼티를 추가할 수 있게 된다.
// js
const person = {
name: '덕배',
age: 30
};
person.job = '개발자'; // 동적으로 프로퍼티 추가
// ts
interface Person {
name: string;
age: number;
[key: string]: any; // 인덱스 시그니처 (Index Signature)
}
const person: Person = {
name: '덕배',
age: 30
};
person.job = '개발자'; // 동적으로 프로퍼티 추가
유틸리티 타입은 이미 정의해 놓은 타입을 변환할 때 사용하기 좋다.
아래 4개의 유틸리티 타입 이외에도 많은 유틸리티 타입이 있으며, 필요에 따라 적절한 유틸리티 타입을 선택해 사용할 수 있다.
interface User {
id: number;
name: string;
email: string;
}
type PartialUser = Partial<User>;
// PartialUser를 이용하여 일부 속성만 사용할 수 있음
const partialUser: PartialUser = { name: 'John' };
interface User {
id: number;
name: string;
email: string;
}
type UserWithoutEmail = Pick<User, 'id' | 'name'>;
// UserWithoutEmail은 id와 name 속성만을 가지는 타입
const userWithoutEmail: UserWithoutEmail = { id: 1, name: 'John' };
interface User {
id: number;
name: string;
email: string;
}
type UserWithoutEmail = Omit<User, 'email'>;
// UserWithoutEmail은 email 속성을 제외한 모든 속성을 가지는 타입
const userWithoutEmail: UserWithoutEmail = { id: 1, name: 'John' };
interface User {
id: number;
name: string;
}
type ReadonlyUser = Readonly<User>;
// ReadonlyUser는 모든 속성이 읽기 전용
const user: ReadonlyUser = { id: 1, name: 'John' };
user.id = 2; // 에러: 읽기 전용 속성이기 때문에 재할당할 수 없음
{
name: string;
age: string;
}
// ↓ 변환
{
name: number;
age: number;
}
자바스크립트의 map() API 함수를 타입에 적용한 것과 같은 효과를 가진다. 자바스크립트 map API는 배열을 다룰 때 유용한 자바스크립트 내장 API이다.
아래 코드는 3개의 객체를 요소로 가진 배열 arr에 map API를 적용한 코드이며, 배열의 각 요소를 순회하여 객체에서 문자열로 변환하였다.
const arr = [{id: 1, title: '함수'}, {id: 2, title: '변수'}, {id: 3, title: '인자'}];
const result = arr.map(function(item) {
return item.title;
});
console.log(result); // ['함수', '변수', '인자']
type MappedType = {
[Property in ExistringType]: NewTypeTransformtion;
}
// Property: 기존 타입의 각 속성을 나타내는 변수
// ExistringType: 변환하려는 기존 타입
// NewTypeTransformtion: 새로운 타입을 생성하기 위한 변환 규칙을 정의하는 부분
// 선택적으로 만드는 맵드 타입
type PartialType<T> = {
[Property in keyof T]?: T[Property];
}
interface User {
age: number;
name: string;
}
// User 타입의 모든 속성을 선택적으로 만듦
type PartialUser = PartialType<User>
// 읽기 전용으로 만드는 맵드 타입
type ReadonlyType<T> = {
readonly [Property in keyof T]: T[Property];
};
interface User {
age: number;
name: string;
}
// User 타입의 모든 속성을 읽기 전용으로 만듦
type ReadonlyUser = ReadonlyType<User>;