- 타입스크립트의 타입
원시타입, 리터럴타입, 배열, 튜플, 객체, 타입 별칭, 인덱스 시그니처, Enum, Any, Unknown, Void, Never
- 타입스크립트 동작 원리
집합&계층, 호환성, 대수 타입, 타입 추론, 타입 단언, 타입 좁히기, 서로소 유니온 타입
참고 | 한 입 크기로 잘라먹는 타입스크립트
타입스크립트가 제공하는 기본 타입들을 계층에 따라 분류한 ‘타입 계층도’ 그림이다. 이런 각각의 기본 타입들은 서로 부모 자식 관계를 이루며 계층을 형성한다.
Never
타입이 공집합, Unknown
타입이 모든 타입의 슈퍼 타입.
치트키인 any
타입도 never
타입으로는 다운캐스팅이 안된다.
원시 타입 (Primitive Type) : 하나의 값만 저장하는 타입
let num1: number = -0.123;
let num1: number1 = NaN;
let num1: number2= -Indifinty;
let str1: string = "hello";
str1 = 123; // ERROR
let bool1: boolean = true;
let null1: null - null;
let numA: number = null; // ERROR. 컴파일옵션에서 strictNullChecks를 false로 설정해주면 ERROR가 아니게 됨
let und1: undefined = undefined;
리터럴 타입 : 값 그 자체를 타입으로 지정하는 타입
let numA: 10 = 10;
numA = 15; // ERROR
배열
let numArr: number[] = [1,2,3];
let strArr: string[] = ["hello", "sir"];
let boolArr: Array<boolean> = [true, false, true]; // Generic이 스임
let multiArr: (number | string)[] = [1, "hello"];
튜플
push
나 pop
을 이용해 고정된 길이를 무시하고 요소를 추가하거나 삭제할 수 있다.각 배열의 0번 인덱스에는 회원의 이름, 1번 인덱스에는 회원의 아이디를 저장해 두었는데 만약 눈치 없는 동료 중 한명이 다음과 같이 순서를 잘 못 배치해 요소를 추가할 경우 문제가 될 수 있다.
자바스크립트에서는 이런 문제를 확인할 방법이 없다. 그러나 타입스크립트에서는 튜플을 사용하면 위와 같은 실수를 빨리 바로잡을 수 있다.
const users = [
["이정환", 1],
["이아무개", 2],
["김아무개", 3],
["박아무개", 4],
[5, "조아무개"], // <- 새로 추가함
];
const users: [string, number][] = [
["이정환", 1],
["이아무개", 2],
["김아무개", 3],
["박아무개", 4],
[5, "조아무개"], // 오류 발생
];
객체 타입 정하기
let user: object = {
id: 1,
name: "이정환:,
};
user.id; // ERROR: 'object' 형식에 'id' 속성이 없다.
왜 이런 에러가..?
그 이유는 타입스크립트의 object 타입은 단순 값이 객체임을 표현하는 것 외에는 아무런 정보도 제공하지 않는 타입이기 때문이다. 따라서 이 타입은 객체의 프로퍼티에 대한 정보를 전혀 가지고 있지 않다. 그렇기 때문에 이렇게 프로퍼티에 접근하려고 하면 오류가 발생한다.
변수 user에 저장된 객체의 구조를 그대로 타입으로 만들고 싶을 때에는 object가 아닌 객체 리터럴 타입을 이용해야 한다.
그러면 객체의 프로퍼티까지 구조적으로 지정할 수 있다.
let user: {
id: number;
name: string;
} = {
id: 1,
name: "이정환",
};
user.id;
타입 별칭
type User = { // 중괄호다 == 객체를 만들거다
id: number;
name: string;
nickname: string;
birth: string;
bio: string;
location: string;
};
let user: User = {
id: 1,
name: "이정환",
nickname: "winterlood",
birth: "1997.01.07",
bio: "안녕하세요",
location: "부천시",
};
let user2: User = {
id: 2,
name: "홍길동",
nickname: "winterlood",
birth: "1997.01.07",
bio: "안녕하세요",
location: "부천시",
};
인덱스 시그니처
참고: https://velog.io/@ahsy92/TypeScript-%EC%9D%B8%EB%8D%B1%EC%8A%A4-%EC%8B%9C%EA%B7%B8%EB%8B%88%EC%B2%98
인덱스 시그니쳐는 속성에 타입을 선언하는 구문과 유사하지만 한 가지 차이점이 존재한다. 속성 이름 대신 대괄호 안에 key타입을 작성하는 것이다.
type userType = {
[key : string] : string
}
let user : userType = {
'마이콜':'사람'
'또치':'타조'
}
type userType = {
[key: string]: string | number | boolean;
}
let user : userType = {
'이름' : '또치'
'나이' : 38
'여자' : true
}
enum 타입
흔하게 사용하지만 굳이 설명하라면..
여러가지 값들에 각각 이름을 부여해 열거해두고 사용하는 타입이다.
열거형 타입은 자바스크립트에는 존재하지 않고 오직 타입스크립트에서만 사용할 수 있는 특별한 타입이다.
// enum 타입
// 여러가지 값들에 각각 이름을 부여해 열거해두고 사용하는 타입
enum Role {
ADMIN = 0,
USER = 1,
GUEST = 2,
}
const user1 = {
name: "이정환",
role: Role.ADMIN, //관리자
};
const user2 = {
name: "홍길동",
role: Role.USER, // 회원
};
const user3 = {
name: "아무개",
role: Role.GUEST, // 게스트
};
enum은 컴파일될 때 다른 타입들 처럼 사라지지 않고 자바스크립트 객체로 변환된다. 따라서 우리가 위에서 했던 것 처럼 값으로 사용할 수 있는 것.
tsc를 이용해 chapter5.ts를 컴파일하고 결과를 살펴보면 우리가 정의한 enum이 다음과 같이 자바스크립트 객체로 변환된 걸 확인할 수 있다.
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["japanese"] = "jp";
})(Language || (Language = {}));
const user1 = {
any 타입
any 타입은 어떠한 타입 검사도 받지 않기 때문에 아무 타입의 값이나 범용적으로 담아 사용할 수 있고 또 다양한 타입의 메서드도 마음대로 호출해서 사용해도 문제가 되지 않는다.
단, any는 최대한 사용하지 말기.
타입 검사를 받지 않는 타입이므로 모든 타입스크립트의 문법과 규칙으로부터 자유롭지만 그만큼 위험한 타입이다. any 타입을 많이 사용하면 많은 부분에서 타입 검사가 제대로 이루어지지 않기에 위험한 코드가 생산됩니다. 이러면 사실 타입스크립트를 사용하는 이유가 없다.
Unknown 타입
unknown 타입은 독특하게도 변수의 타입으로 정의되면 모든 값을 할당받을 수 있게 되지만, 반대로 unknown 타입의 값은 그 어떤 타입의 변수에도 할당할 수 없고, 모든 연산에 참가할 수 없게 된다. 쉽게 정리하면 오직 값을 저장하는 행위밖에 할 수 없게 된다.
만약 unknown 타입의 값을 number 타입의 값처럼 취급하고 곱셈 연산을 수행하게 하고 싶다면 이 값이 number 타입의 값임을 보장해줘야 한다.
특정 변수가 당장 어떤 값을 받게 될 지 모른다면 any 타입으로 정의하는 것 보단 unknown 타입을 이용하는게 훨씬 안전한 선택이 된다
void 타입 : 공허 타입. 아무것도 없음을 의미하는 타입.
⇒ TypeScript는 이 함수가 아무것도 return하지 않는다는 것을 자동으로 인식하게 된다.
never 타입 : 불가능을 의미하는 타입.
왜 쓰지?
함수가 절대 return하지 않을 때 발생하게 된다.
⇒ 함수에서 예외가 발생할 때 사용.
function hello() : never {
return "x" // 오류 발생
throw new Error("xxx") // 실행
}
타입스크립트를 이해한다는 것은?
타입스크립트를 이해한다는 말은 타입스크립트가 어떤 기준으로 타입을 정의하고, 어떤 기준으로 타입들간의 관계를 정의하고, 어떤 기준으로 타입스크립트 코드의 오류를 검사 하는지 그 원리와 동작 방식을 낯낯이 살펴본다는 말이다.
타입스크립트의 '타입'은 사실 여러개의 값을 포함하는 '집합'이다. 집합은 동일한 속성을 갖는 여러개의 요소들을 하나의 그룹으로 묶은 단위를 말한다. 따라서 여러개의 숫자 값들을 묶어 놓은 집합을 타입스크립트에서는 number 타입이라고 부른다.
타입스크립트가 제공하는 여러가지 기본 타입들간의 집합으로써의 부모-자식 관계이다.
타입 호환성이라는 걸 언급할 때이다.
타입스크립트에서는 이렇게 슈퍼타입의 값을 서브타입의 값으로 취급하는 것을 허용하지 않고, 반대로는 허용한다.
서브 타입의 값을 슈퍼 타입의 값으로 취급하는 것은 업 캐스팅 이라고 부르고 반대는 다운 캐스팅이라고 부른다. 따라서 쉽게 정리하면 업 캐스팅은 모든 상황에 가능하지만 다운 캐스팅은 대부분의 상황에 불가능하다고 할 수 있다.
객체 타입의 호환성
어떤 객체 타입을 다른 객체 타입으로 취급해도 괜찮은가 ?
초과 프로퍼티 검사가 진행됨.
대수 타입(Algebraic type)이란
대수 타입이란 여러개의 타입을 합성해서 만드는 타입.
대수 타입에는 합집합 타입과 교집합 타입이 존재한다.
합집합은 Union 타입, 교집합은 Intersection 타입이라고 부른다.
Union 타입은 누구도 서로의 슈퍼 타입이거나 서브 타입이지 않고 그냥 교집합을 가지는 타입이다.
type Dog = {
name: string;
color: string;
};
type Person = {
name: string;
language: string;
};
type Union1 = Dog | Person;
(...)
let union1: Union1 = { // ✅
name: "",
color: "",
};
let union2: Union1 = { // ✅
name: "",
language: "",
};
let union3: Union1 = { // ✅
name: "",
color: "",
language: "",
};
let union4: Union1 = { // ❌
name: "",
};
이번엔 교집합 타입을 보자,
대다수의 기본 타입들 간에는 서로 공유하는 교집합이 없기 때문에 이런 Intersection 타입은 보통 객체 타입들에 자주 사용된다.
서로 교집합을 공유하지 않는 서로소 집합이므로 변수 variable의 타입은 결국 never 타입으로 추론된다.
let variable: number & string;
// never 타입으로 추론된다
...
type Dog = {
name: string;
color: string;
};
type Person = {
name: string;
language: string;
};
type Intersection = Dog & Person;
let intersection1: Intersection = {
name: "",
color: "",
language: "",
};
타입 추론
타입스크립트는 타입이 정의되어 있지 않은 변수의 타입을 자동으로 추론한다. 이런 기능을 “타입 추론”이라고 한다.
타입 단언
업캐스팅, 다운캐스팅처럼 타입 변환이 아니라 타입스크립트 컴파일러에 안대를 씌워주는 것이다. 위험한 문법임
타입 단언에도 조건이 있다.
값 as 타입 형식의 단언식을 A as B로 표현했을 때 아래의 두가지 조건 중 한가지를 반드시 만족해야 한다.
let num1 = 10 as never; // ✅
let num2 = 10 as unknown; // ✅
let num3 = 10 as string; // ❌
number 타입과 string 타입은 서로 슈퍼-서브 타입 관계를 갖지 않는다. 따라서 단언이 불가하다.
다중 단언 (정말 어쩔 수 없이 필요한 상황에서만 이용하기를 권장)
타입 단언은 다중으로도 가능하다. 다중 단언을 이용하면 앞서 살펴본 예제 중 불가능했던 단언을 다음과 같이 가능하도록 만들 수도 있다.
let num3 = 10 as unknown as string;
이런 다중 단언의 경우 왼쪽에서 오른쪽으로 단언이 이루어진다. 따라서 순서대로 살펴보면 다음과 같다.
number 타입의 값을 unknown 타입으로 단언한다.
unknown 타입의 값을 string 타입으로 단언한다.
const 단언
let num4 = 10 as const;
// 10 Number Literal 타입으로 단언됨
let cat = {
name: "야옹이",
color: "yellow",
} as const;
// 모든 프로퍼티가 readonly를 갖도록 단언됨
Non null 단언
Non Null 단언은 지금까지 살펴본 값 as 타입 형태를 따르지 않는 단언이다. 값 뒤에 느낌표(!) 를 붙여주면 이 값이 undefined이거나 null이 아닐것으로 단언할 수 있다.
type Post = {
title: string;
author?: string; // author 프로퍼티를 선택적 프로퍼티로 정의
};
let post: Post = {
title: "게시글1",
};
const len: number = post.author!.length; // 옵셔널 체이닝 대신 있어! 라고 표현
타입 좁히기: 조건문 등을 이용해 넓은 타입에서 좁은 타입으로, 타입을 상황에 따라 좁히는 방법을 이야기함.
메모) 날짜를 저장하는 Date 객체처럼 Node.js가 기본적으로 제공하는 내장 객체들에 대해서는 타입들이 다 기본적으로 제공됨.
우리가 직접 만든 타입과 함께 사용하려면 다음과 같이 instanceof 대신, in 연산자를 이용해야 한다.
type Person = { // 클래스가 아니라, 그냥 우리가 타입 별칭으로 만든 객체 타입.
name: string,
age: number,
}
function func (value: number | string | Date | null | Person) {
if (typeof value === "number"){
console.log(value.toFixed());
}else if (typeof value === "string"){
console.log(value.toUpperCase());
}else if (typeof value === "object") {
// 권장 X
}else if (value instanceof Date) {
console.log(value.getTime());
}else if (value && "age" in value) {
console.log(`${value.name}은 ${value.age}살 입니다`)
}
}
서로소 유니온 타입
교집합이 없는 타입들 즉 서로소 관계에 있는 타입들을 모아 만든 유니온 타입
언제 쓸모가 있냐?
type Admin = {
name: string;
kickCount: number;
};
type Member = {
name: string;
point: number;
};
type Guest = {
name: string;
visitCount: number;
};
type User = Admin | Member | Guest;
function login(user: User) {
if ("kickCount" in user) {
// Admin
console.log(`${user.name}님 현재까지 ${user.kickCount}명 추방했습니다`);
} else if ("point" in user) {
// Member
console.log(`${user.name}님 현재까지 ${user.point}모았습니다`);
} else {
// Guest
console.log(`${user.name}님 현재까지 ${user.visitCount}번 오셨습니다`);
}
}
그러나 이렇게 코드를 작성하면 조건식만 보고 어떤 타입으로 좁혀지는지 바로 파악하기가 좀 어렵다. 결과적으로 직관적이지 못한 코드이다.
이럴 때에는 다음과 같이 각 타입에 태그 프로퍼티를 추가 정의해주면 된다.
type Admin = {
tag: "ADMIN";
name: string;
kickCount: number;
};
type Member = {
tag: "MEMBER";
name: string;
point: number;
};
type Guest = {
tag: "GUEST";
name: string;
visitCount: number;
};
(...)
Admin 타입에는 “ADMIN” String Literal 타입의 tag 프로퍼티를,
Member 타입에는 “MEMBER” String Literal 타입의 tag 프로퍼티를,
Guest 타입에는 “GUEST” String Literal 타입의 tag 프로퍼티를 각각 추가한다.
그럼 이제 login 함수의 타입가드를 다음과 같이 더 직관적으로 수정할 수 있게 된다.
function login(user: User) {
switch (user.tag) {
case "ADMIN": {
console.log(`${user.name}님 현재까지 ${user.kickCount}명 추방했습니다`);
break;
}
case "MEMBER": {
console.log(`${user.name}님 현재까지 ${user.point}모았습니다`);
break;
}
case "GUEST": {
console.log(`${user.name}님 현재까지 ${user.visitCount}번 오셨습니다`);
break;
}
}
}