
지난 시간에 원시 타입(Primitive Type)에 대해 알아봤다면,
이번에는 비원시(Non-Primitive) 타입에 대해 정리해보자!
타입스크림트에서 배열 타입을 정의하고 사용하는 방법에 대해 알아보자.
먼저 코드를 보면,
let numArr: number[] = [1,2,3];
배열요소타입[] 형식으로 배열 타입을 정의할 수 있다.
또는 제네릭(Generic) 문법을 사용해서도 정의할 수 있다.
let boolArr: Array<Boolean> = [true,false];
📌 제네릭(Generic) 이란?
Array<요소타입> 형태로 타입을 정의하는 방식!
제네릭에 대해선 나중에 알아보자
만약에 아래 코드와 같이 다양한 타입의 배열 요소를 갖는 배열 타입을 정의할 때는 어떻게 해야할까?
let multiArr = [1, "hello"];
이럴 때에는 소괄호의 바(|)를 이용해서 배열 요소가 둘 중 하나의 타입에 해당하도록 타입을 정의하면 된다.
let multiArr: (number|string)[] = [1,"hello"];
이렇게 정의한 배열 타입은 요소가 number 타입이거나 string 타입이어야 한다.
이렇듯 바(|)를 이용해서 여러 타입중 하나를 만족하는 타입을 정의하는 문법을 유니온(Union) 타입이라고 부른다.
다차원의 배열도 간단하게 정의할 수 있다.
let arr: number[][] = [
[1,2,3],
[4,5],
];
튜플은 길이와 타입이 고정된 배열을 의미한다.
let tup: [number,number] = [1,2];
let tup: [number, string, boolean]= [1, "hello", true];
tsc를 이용해서 튜플 타입이 정의된 타입스크립트 코드를 컴파일 해서 보면 결국 튜플은 자바스크립트 배열로 변환되는 것을 확인할 수 있다.
튜플도 결국 배열이므로
push나pop을 사용할 수 있지만, 조심해야 한다!
잘못하면 고정된 길이를 깨뜨릴 수도 있다.
다음으론 객체 타입에 대해서 알아보자.
먼저 객체를 하나 만들어보자.
let user = {
id: 1,
name: '홍길동'
}
객체의 타입은 object 이므로 object 를 써주면 된다.
let user:object = {
id: 1,
name: "홍길동"
}
user.id // -> 오류 발생
여기서 객체의 프로퍼티에 접근을 하려고 하면 오류가 발생을 한다.
그 이유는 타입스크립트의 object 타입은 단순 값이 객체임을 표현하는 것 외에는 아무런 정보도 제공하지 않는 타입이기 때문이다! 따라서 이 타입은 객체의 프로퍼티에 대한 정보를 전혀 가지고 있지 않다.
그러면 어떻게 접근할 수 있을까? 바로 객체 리터럴 타입을 이용해야 한다.
let user:{
id: number,
name: string,
} = {
id: 1,
name: "홍길동",
};
user.id;
변수 user의 타입을 number 타입의 id, string 타입의 name 프로퍼티를 갖는 객체 타입으로 정의를 하면, 이제 타입 내에 정의되어있는 프로퍼티에 접근할 수 있다.
여기서 한가지 알 수 있는 사실은 타입스크립트는 기존의 정적 타입 시스템을 따르는 언어인 C나 Java와는 달리 객체의 타입을 정의할 때 프로퍼티를 기준으로 객체의 구조를 정의하듯이 타입을 정의한다는 점이다.
이러한 특징을 구조적 타입 시스템이라고 부른다. 객체의 구조를 결정하는 것은 프로퍼티이다. 따라서 객체에 어떤 프로퍼티들이 있어야 하는지 정의하는 방식으로 객체 타입을 정의해야한다.
자바스크립트에서 객체를 다루다보면 자주 특정 프로퍼티는 있어도 되고 없어도 되는 그러한 상황이 존재한다. 하지만 타입스크립트에서는 오류가 발생을 한다. 아래 코드를 봐보자.
let user: {
id: number,
name: string,
} = {
id: 1,
name: "홍길동",
};
user = {
name:"심봉사" // 오류 발생.
}
오류가 발생한 이유는 user의 타입은 id와 name 프로퍼티가 있는 객체인데 마지막 라인에서 name만 존재하는 객체를 할당했기 때문이다.
이렇게 특정 프로퍼티를 상황에 따라 생략하도록 만들고 싶다면 선택적 프로퍼티로 만들어줘야 하는데, 방법은 프로퍼티 이름 뒤에 ?를 붙여주면 된다.
let user: {
id?: number,
name: string,
} = {
id: 1,
name: "홍길동",
};
user = {
name:"심봉사"
}
❗ 한 가지 주의할 점은 선택적 프로퍼티가 만약 존재한다면 그때의 value 타입은 반드시 number 타입이어야 한다는 점이다.
let user: {
id?: number,
readonly name: string,
} = {
id: 1,
name: "홍길동",
};
user.name = "example" // 오류 발생
name 프로퍼티는 읽기 전용 프로퍼티가 되었기 때문에 마지막 라인처럼 프로퍼티의 값을 수정하려고 하면 오류가 발생하게 된다.
타입 별칭을 이용하면 다음과 같이 변수를 선언하듯 타입을 별도로 정의할 수 있다.
type User = {
id: number,
name: string,
nickname: string,
birth: string,
bio: string,
location: string,
}
type User ={} // -> 불가능
function test() {
type User = string; // 가능
}
type 타입_이름 = 타입 형태로 타입을 정의한다.
❗ 참고로 동일한 스코프에 동일한 이름의 타입 별칭을 선언하는 것은 불가능하다.
스코프가 다르다면 여러개의 별칭을 선언해도 상관 없다.
객체의 프로퍼티를 동적으로 정의할 때 사용한다.
예를 들어 다양한 사람들의 국적을 가지고 있는 객체가 있다고 가정하자.
type PeopleCountry = {
UnitedState: string,
Korea: string,
UK: string,
}
let peopleCodes: PeopleCountry = {
UnitedState: "us",
Korea: "ko",
UK: "uk",
}
만약에 이때 peopleCodes 에 수없이 많은 프로퍼티가 추가되어야 한다면 타입 정의에도 각 프로퍼티를 모두 정의를 해주어야하기 때문에 매우 불편할 것이다..
바로 이럴 때 인덱스 시그니처를 사용해주면 간단하게 타입을 정의할 수 있다.
type PeopleCountry = {
[key: string] : string;
}
let peopleCodes: PeopleCountry = {
UnitedState: "us",
Korea: "ko",
UK: "uk",
}
[key: string] : string 이 인덱스 시그니처 문법이고, 이 객체 타입에는 key 가 string 타입이고 value가 string 타입인 모든 프로퍼티를 포함된다 라는 의미이다.
❗ 주의!
인덱스 시그니처를 사용할 때 추가적인 프로퍼티를 정의하면,
해당 프로퍼티의 타입이 인덱스 시그니처의value타입과 호환되어야 한다!
여러 개의 값을 나열하는 TypeScript 전용 타입이다.
enum Role {
ADMIN = 0,
USER = 1,
GUEST = 2,
}
// 각 멤버에는 숫자도 할당할 수 있다.
// 할당을 안해도 자동할당이 된다!!
enum Role {
ADMIN, // 0,
USER, //1,
GUEST, //2,
}
// 아래와 같이 enum 멤버들을 값으로도 활용이 가능하다.
const user1 = {
name: "홍길동",
role: Role.ADMIN, //관리자
};
const user2 = {
name: "심봉사",
role: Role.USER, // 회원
};
const user3 = {
name: "심청이",
role: Role.GUEST, // 게스트
};
enum 멤버에는 숫자 말고도 문자열 값도 할당할 수 있다.
enum Role {
ADMIN,
USER,
GUEST,
}
enum Language {
korean: "ko",
english: "en",
}
const user1 = {
name: "홍길동",
role: Role.ADMIN,
langugae: Language.korean
};
Enum은 컴파일 후에도 JavaScript 객체로 남는다.
👉 컴파일 후 Role은 일반 객체로 변환된다.
var Role;
(function (Role) {
Role[Role["ADMIN"] = 0] = "ADMIN";
Role[Role["USER"] = 1] = "USER";
Role[Role["GUEST"] = 2] = "GUEST";
})(Role || (Role = {}));
var Language;
(function (Language) {
Language["korean"] = "ko";
Language["english"] = "en";
})(Language || (Language = {}));
any 타입은 타입 검사를 받지 않는 특수한 타입이다.
👉
any는 값 자체라기보다는 타입스크립트의 타입 시스템에서 타입을 표현하는 하나의 개념이라서 비 원시 타입으로 분류한다.
예를 들어서 다음과 같이 범용적으로 사용되어야 하는 변수가 하나 있다고 가정하자.
let anyVar = 10;
anyVar = "hello"; // 오류 발생
anyVar 는 number 타입의 값으로 10으로 초기화 했지만 이후에 string 타입의 값 "hello" 를 저장해야 한다고 가정을 해보자.
타입스크립트에서는 변수의 타입이 변수를 초기화할 때 초기화 하는 값을 기준으로 추론하기 때문에 이렇게 하면 오류가 발생한다.
이럴 때에는 any 라는 타입을 이용하면 된다.
let anyVar: any = 10;
anyVar = "hello";
anyVar = true;
anyVar = {};
anyVar.toUpperCase();
anyVar.toFixed();
anyVar.a;
어떠한 타입 검사도 받지 않기 때문에 아무 타입의 값이나 범용적으로 담아 사용할 수 있고, 다양한 타입의 메서드도 마음대로 호출해서 사용해도 문제가 되지 않는다.
❗ 하지만 any 는 최대한 사용하지 않는게 좋다.
타입스크립트의 문법과 규칙으로부터 자유로워서, 그만큼 위험한 타입이기 때문이다!
unknown 타입은 any 타입과 비슷하지만 보다 안전한 타입이다.
unknwon 타입의 변수는 어떤 타입의 값이든 다 저장할 수 있다.
unknown은 원시 타입이 될 수도 있고 객체 타입이 될 수도 있지만, 타입스크립트의 타입 시스템에서 동작하는 특별한 타입이므로 비 원시 타입이다.
let unknownVar: unknown;
unknownVar = "";
unknownVar = 1;
unknownVar = () => {};
// 하지만 그 반대로는 안된다.
let num: number = 10;
let unknownVar: unknown;
unknownVar = "";
unknownVar = 1;
unknownVar = () => {};
num = unknownVar; // 오류 !
✔ 모든 값을 저장 가능하지만, 다른 타입 변수에 할당할 수 없다.
만약에 number 타입의 값처럼 취급하고 곱셉 연산을 수행하게 하고 싶다면 typeof 체크 후 안전하게 사용 가능!
if (typeof unknownVar === "number"){
unknwonVar * 2;
}
void 타입은 아무런 값도 없음을 의미하는 타입이다.
👉
void자체는 값을 가질 수 없고, 타입스크립트의 타입 시스템에서만 의미가 있는 타입이라 비 원시 타입으로 분류된다.
function func1(): void {
console.log("Hello");
}
물론 다음과 같이 변수의 타입으로도 void 타입을 지정할 수 있다. 그러나 void 타입의 변수에는 undefiend 이외의 타입의 값을 담을 수 없다. 그 이유는 void 타입이 undefined 타입을 포함하는 타입이기 때문이다.
let a: void;
a = undefined;
이때, tsconfig.json 에 strictNullCheck 옵션을 false 로 설정하면 void 타입의 변수에도 null 값을 담을 수가 있게 된다.
never 타입은 불가능을 의미하는 타입이다.
보통 다음과 같이 함수가 어떠한 값도 반환할 수 없는 상황일 때 사용된다.
function func1(): never{
while(true) {}
}
func1() 는 무한 루프를 돌기 때문에 아무런 값도 반환할 수 없다. 엄밀히 말하자면 이 함수는 영원히 종료될 수 없기 때문에 무언가를 반환한다는 것 자체가 '불가능' 하다.
또한 의도적으로 오류를 발생시키는 함수도 never 타입으로 반환값 타입을 정의할 수 있다.
function func2(): never {
throw new Error();
}
❗ 변수의 타입을 never 로 정의하면 any 를 포함해 그 어떠한 값도 이 변수에 담을 수 없게 된다.
let anyVar: any;
let a: never;
a = 1; // X
a = null; // X
a = undefined; // X
a = anyVar; // X
✔ 배열, 튜플, 객체, Enum 등 다양한 비원시 타입을 배웠다!
✔ any 타입은 사용을 피하고, unknown 타입을 더 안전하게 활용하자!
✔ void와 never 타입은 반환값이 없거나, 절대 반환되지 않는 경우에 사용된다.
🔥 다음 블로그에서 다른 개념을 정리해보자!