타입을 정의하는 것은 매우 간단하고 쉬운 일이다. 변수나 함수의 명칭 뒤에 :을 사용하고 타입을 작성해주기만 하면 된다.
// 문자열
const str: string = 'hello';
// 숫자
const num: number = 10;
// 제네릭 사용
const arr: Array<string> = [];
arr.push('hi');
// 리터럴 사용
const items: string[] = [];
배열 타입은 제네릭을 사용하지 않으면 타입 선언이 불가능하다. 제네릭에 대한 내용은 뒤에서 다룬다..
튜플?
배열의 길이가 고정되고 각 요소의 타입이 지정되어 있는 배열 형식을 의미한다.
// 튜플
let address: [string, number] = ['판교', 40];
// 객체
const obj: object = {};
// key-value가 같이 선언된 객체
const obj2: object = {
name: 'name',
age: 100
};
// key-value가 같이 선언되고, 각 key들의 타입까지 선언된 객체
const obj3: { name: string, age: number} = {
name: 'name',
age: 100
};
// 진위값
let isLogin: boolean = false;
타입스크립트에서 함수에는 크게 3가지 타입을 정의할 수 있다.
함수의 파라미터(매개변수) 타입.
function add(a: number, b: number) {
return a + b;
}
함수의 반환 타입.
function add(a: number, b: number): number {
return a + b;
}
위 함수의 매개변수 타입은 숫자, 그리고 함수에서 반환하는 값의 타입은 숫자이다. 반환값의 타입이 정의되었다면, 함수의 반환값이 없거나 다른 타입의 데이터를 반환할 수 없다.
함수의 구조 타입.
function log(a: string) {
console.log(a);
}
자바스크립트에서는 정해진 매개변수보다 많은 인자가 들어와도 문제가 없다. JS 엔진에서 필요한 인자만 받아서 매개변수로 사용해주기 때문.
그런데 타입스크립트는 인자의 갯수도 체크의 대상으로 삼는다. 매개변수는 하나인데, 인자가 2개 들어오면? 에러가 발생한다.
매개변수가 명확하게 정해져있지 않는 경우에는 어떻게 해야할까? 상황에 따라서 인자가 더 추가될 수도 있다면? 동일하지만 인자가 다른 각각의 함수를 구현할 수도 있지만, 이럴 때는 옵셔널 파라미터를 사용하면 된다.
function printText(text: string, text2?: string) {
console.log(text);
}
printText('hi');
printText('hi', 'hi2');
옵셔널 파라미터로 선언된 인자는 있어도 되고, 없어도 에러를 발생시키지 않는다. 당연하지만 옵셔널 파라미터가 없는 매개변수 자리에 인자를 집어넣는 것은 에러가 발생한다.
printText('hi', 'hi2', 'hi3'); // 에러!
코드의 규모가 작다면 위에서 설명했던 방식만 가지고 타입을 정의할 수 있다. 그런데 App에서 다루는 데이터가 많고, 이들의 타입을 하나하나 지정해주는 것은 대단히 비효율적인 일이다. 따라서 이런 경우에는 인터페이스 개념을 사용한다.
interface User {
name: string;
age: number;
}
App에서 최초 한번 인터페이스를 통해 해당 객체에는 어떤 데이터가 있고, 이들의 타입은 무엇인지를 정의해줄 수 있다. 이렇게 되면 이 인터페이스는 해당 App에서 사용이 가능한 하나의 타입이 된다.
// 변수에 사용하는 경우
const seho: User = {
name: 'hi',
age: 100
};
// 함수의 매개변수에 사용하는 경우
function getUser(user: User) {
console.log(user);
}
getUser(seho);
interface SumFunction {
(a: number, b: number): number;
}
SumFunction의 인자는 a와 b가 있으며 이들의 타입은 number이다. 또한 함수의 반환값의 타입 역시 number이다.
let sum: SumFunction;
sum 변수의 타입 형식은 interface SumFunction를 따르는 함수이다. 그리고 타입에 맞게 sum에 함수를 대입하면 아래와 같은 형태가 완성된다.
sum = function (a: number, b: number): number {
return a + b;
};
interface StringArray {
[index: number]: string;
}
배열의 index는 숫자이며, value는 문자열이다.
let arr: StringArray = [];
arr[0] = 'hi';
arr[1] = 'hi2';
인터페이스에서 정의한 타입에 맞게 배열을 선언하고, 타입에 맞는 요소를 추가한다. 이런 방식을 딕셔너리 패턴Dictionary Pattern이라고 부른다.
interface Person {
name: string;
age: number;
}
interface Developer extends Person {
language: string;
}
자바스크립트의 클래스 상속과 동일한 원리와 구조로 동작한다. interface Developer는 Person의 내용을 모두 가지고 있다.
class Person2 {
private name: string;
public age: number;
readonly log: string;
constructor(name, age) {
this.name = name;
this.age = age;
}
}
타입스크립트에서의 클래스는 접근 제한자를 사용할 수 있게 해준다. private, public, readonly가 바로 그것.
타입 별칭 (Type Aliases)
타입 별칭은 특정 타입이나 인터페이스를 참조할 수 있는 타입 변수를 의미한다.
이 두 개념은 서로 같은 기능을 수행한다. 그런데 왜 동일한 기능을 하는 개념이 2가지 존재하는 것인가? 이 둘의 차이점은 다음과 같다.
클래스나 객체의 구조를 정의하고, 클래스가 해당 인터페이스를 구현할 때 사용됩니다.
상속과 확장이 가능하므로 다른 인터페이스를 기반으로 확장된 인터페이스를 만들 수 있습니다.
클래스 구조를 정의하거나 외부 라이브러리의 타입을 확장하는 용도로 적합합니다.
객체의 구조를 정의하고 해당 구조를 따르는 여러 클래스 또는 객체를 다룰 때 사용하기 좋습니다.
여러 타입을 조합하거나, 복잡한 타입을 정의하고 다루는 용도로 적합합니다.
유니온, 인터섹션, 튜플 등 다양한 타입을 정의할 수 있습니다.
더 간단하게 타입을 선언하고 활용할 수 있어 코드 가독성을 높일 수 있습니다.
확장이 불가능하므로 타입을 다른 타입에 기반하여 확장해야 할 경우에는 인터페이스를 사용하는 것이 적합합니다.
상황과 요구 사항에 맞게 선택하여 사용하는 것이 좋다.
interface Person1 {
name: string;
age: number;
};
type Person2 = {
name: string;
age: number;
};
인터페이스와 타입 별칭으로 타입을 정의해주고..
let kim: Person1 = {
name: '김',
age: 20,
};
let lee: Person2 = {
name: '이',
age: 20,
};
이를 이용하여 객체를 하나씩 선언해준 다음 VSCode 상에서 프리뷰로 살펴보면..
interface Person1
type Person2 = {
name: string;
age: number;
}
인터페이스는 let kim이 interface Person1 형식이라고만 알려주지만, let lee는 내부에 정의한 타입까지 프리뷰에서 알려준다.
타입스크립트에서는 타입을 하나만 지정하여 다른 타입의 데이터가 사용되었을 때 에러를 발생시켜 이를 막아준다.
데이터의 타입을 검사할 수 있는 것은 타입스크립트의 장점이지만, 반대로 자바스크립트의 유연함이라는 장점을 없애는 단점으로 작용할 수도 있다. 이럴 때 사용할 수 있는 것이 any 타입이지만, 이렇게 되면 타입스크립트의 타입 검사의 의미가 사라지고 사실상 타입스크립트를 사용하는 이유가 없어져버린다.
이럴 때 사용할 수 있는 것이 바로 Union 타입. Union 타입은 any보다 더 명시적이지만 일반적인 타입 정의에 비해 유연함을 갖는다.
function logMessage(value: string | number) {
console.log(value);
}
이 함수의 매개변수는 문자열 혹은 숫자 데이터만 사용할 수 있다. 하나의 타입만을 정의하는게 아니라, 복수의 특정 타입들을 사용할 수 있게 하는 것. 타입스크립트의 엄격함과 자바스크립트의 유연함을 반반 섞었다고 할 수 있다.
function logMessage(value: string | number) {
if (typeof value === 'string') {
value.toLocaleUpperCase();
}
if (typeof value === 'number') {
value.toLocaleString();
}
throw new TypeError('value must be string or number')
}
해당 데이터가 어떤 타입인지 범위를 좁혀나가는 과정. 예시를 들어 설명하자면, 유니온 타입으로 타입을 정의했다고 해도 그건 인자와 매개변수 단위에서 유연함을 제공하는 것이지 함수 내부 등에서 여러 타입을 동시에 사용했을 때 에러까지 방지해준다는 것은 아니다.
유니온 타입을 사용했을 때는 데이터 타입으로 인한 에러가 발생하지 않도록 위 코드와 같이 상황에 맞는 데이터를 구별할 장치가 마련되어있어야 한다. 이를 타입 가드라고 한다.
interface Developer {
name: string;
skill: string;
}
interface Person {
name: string;
age: number;
}
function askSomeone(someone: Developer | Person) {
someone.age; // 에러!
someone.name; // 문제 없음.
}
이렇게 두 인터페이스를 유니온 타입으로 묶어 사용하였을 때, 두 인터페이스에 있는 모든 요소들을 접근할 수 있다고 생각할 수 있지만 사실 그렇지가 않다. 타입스크립트 입장에서는 두 인터페이스에서 동시에 존재하는 요소 이외의 것이 사용될 경우에는 에러를 발생시킬 가능성이 존재하기 때문에, 위 경우에서는 두 인터페이스에서 동시에 존재하는 요소만 접근이 가능하다.
유니온 타입과 반대의 특징을 가지는 타입. 유니온 타입과 달리 하나의 인터페이스에만 존재하는 요소도 접근이 가능하다.
function askSomeone2(someone: Developer & Person) {
someone.age;
someone.skill;
}
다만 인터섹션 타입의 경우 위 함수를 호출 할 때, 인터페이스의 일부 요소가 없어도 호출이 가능한 유니온 타입과 달리 모든 인터페이스의 요소를 인자로 사용해야만 호출이 가능하다는 단점이 존재한다.