[TypeScript] - (2) Typescript 기본
본 포스팅은 Inflearn(인프런) 이정환님의 TypeScript 강의를 기반으로 작성되었습니다.
[원시 Type과 리터럴 Type]
String Type
const a:string = "Hello";
Number Type
const a:number = 10;
Boolean Type
const a:boolean = false;
Null Type
const a:null = null;
Undefined Type
const a:undefined = undefined;
Literal Type
- "literal == 값"이기 때문에 특정 값으로만 지정된 타입을 의미한다.
- 이 타입은 많이 사용되지는 않는다.
let num2: 10 = 10;
let str2: "hi" = "hi";
let bool: true = true;
tsconfig.json 파일의 "compilerOptions"에 "strictNullChecks" 속성에 true를 입력하면 null type에 대한 검사를 엄격하게 진행한다는 설정이고 false를 입력하면 검사를 하지 않겠다는 설정이 된다.
"compilerOptions" 속성에는 "strict"라는 속성이 있는데 "strict"라는 단어가 들어간 모든 속성들의 최상위 속성이므로 strictNullChecks":false 같이 별도로 값을 지정해주지 않으면 모든 strict 관련 속성들은 "strict"에 할당된 값으로 적용이 된다.
[배열과 튜플 Type]
Array Type
- 일반 타입 선언과 제네릭 타입 선언이 있지만 일반 타입 선언이 코드의 길이를 단축시키기에 좋다.
const arr:number[] = [1,2,3];
//제네릭 타입
const arr:Array<number> = [1,2,3];
배열에 들어가는 요소의 타입이 한가지가 아닌 경우
- Union Type(유니온 타입) 사용
//Union Type
const arr:(number | string)[] = [10,'hello'];
다차원 배열
- 내부 배열의 타입을 정해준 후 배열의 깊이에 맞게 대괄호를 연속적으로 붙여준다.
const arr:number[][] = [
[1,2,3],
[4,5]
];
Tuple(튜플)
- 길이와 타입이 고정된 배열
const arr:[number,string] = [10,'hello'];
const arr1:[number,string,boolean] = [10,'a',true];
const arr1:[string,boolean,number] = [10,'a',true]; //Error
튜플은 TS에만 존재하는 자료형으로 튜플을 사용한 TS 파일을 컴파일하여 JS 파일로 변환 시 일반 배열 형태로 컴파일된다. 튜플은 사실상 일반 배열과 같으며 길이가 고정되어 있음에도 JS에서 사용하는 배열 method도 동일하게 사용이 가능하다. 따라서 튜플을 사용할 때에는 각별한 주의가 필요하다.
튜플은 배열을 사용할 때에 배열 안의 요소들의 위치가 정해져 있고 요소들의 순서가 중요한 경우에 사용하기에 용이하다.
//Tuple Type 예시
const user:[string,number][] = [
['tom',1],
['mike',1],
['jun',1],
['kelly',1],
[5,'bob'], //Error
]
[객체 Type]
Object
객체 타입은 타입명을 object로 정의하여 사용할 수 있다. 하지만 객체는 기본적으로 내부 property들을 보유하고 있다. object로 타입을 지정해두고 해당 객체의 속성과 속성값에 접근하려고 하면 오류가 발생한다. 예시는 다음과 같다.
const user:object = {
id: 1,
name: 'Tom'
}
user.id; //Error
object로 타입을 지정하면 변수 자체를 객체로는 취급하지만 내부 속성에 대해서는 아무 정의도 하지 않기 때문이다.
따라서 객체 타입을 정의하기 위해서는 객체 리터럴 타입으로 정의해주어야 한다.
//Object Literal Type
const user: {
id: number,
name: string
} = {
id: 1,
name: 'Tom'
}
위와 같이 객체 리터럴 타입으로 정의를 해주어야하는 이유는 TS는 객체를 정의할 때 객체와 그 내부의 속성을 따지는 즉, 객체의 구조를 따져서 타입을 정의하기 때문에 구조적 타입 시스템을 따르기 때문이다.
(*C언어와 Java 같은 언어는 이름에 따른 타입을 정의하기 때문에 명목적 타입 시스템을 따른다고 한다.)
Optional Property(선택적 속성)
객체를 생성하다보면 내부 속성중에 필수 속성도 있지만 있어도 되고 없어도 되는 선택적인 속성도 존재하기 마련이다. 그런 속성을 다루기 위한 방식이 선택적 속성(Optional Property)이다. 선택적 속성은 ES6 문법에 존재하는 Optional Chaining을 사용한다. 사용 방법은 아래와 같다.
const user:{
name:string;
age:number;
} = {
name: 'Tom'
} //Error
//Optional Property(with Optional Chaining)
const user:{
name:string;
age?:number;
} = {
name: 'Tom'
}
Readonly
객체를 다루다보면 객체 내부 속성의 속성값을 변경하는 경우도 발생한다. TS에서도 당연히 정의된 타입과 변경하고자 하는 속성값의 타입이 일치한다면 문제 없이 가능하지만 개발을 하는 과정에서 절대적으로 변경되어서는 안되는 객체의 속성값들을 안전하게 수정 불가 상태로 보호하는 방식이 존재하는데, 그것이 바로 Readonly 키워드이다.
Readonly는 말 그대로 '읽기 전용' 속성이므로 속성값을 변경할 수 없도록 막아준다. 사용 방법은 아래와 같다.
const config:{
api: string;
} = {
api: "a123b";
}
config.api = "hello";
console.log(config.api) //"hello"
//Readonly
const config:{
readonly api: string;
} = {
api: "a123b";
}
config.api = "hello"; //Error
console.log(config.api) //"a123b"
Readonly 속성은 배열,튜플,객체의 속성에만 적용이 가능하며 원시 타입의 자료형에는 사용이 불가능하다.
[타입 별칭(Type Alias)과 인덱스 시그니처]
Type Alias(타입 별칭)
Type Alias(타입 별칭)란 불필요한 타입 정의의 반복을 피하기 위해 사용되는 방식이다. 다양한 변수들을 선언하고 타입을 정의해주는 경우에 같은 테마의 변수들은 내부 속성들의 구조와 타입들이 같을 것이다. 그러한 경우에 타입을 반복적으로 사용할 필요 없이 Type Alias를 사용하면 된다. 사용 예시는 다음과 같다.
let user:{
id:number,
name: string,
nickname: string,
birth: number,
bio: string,
location: string
} = {
id: 1,
name:'mike',
nickname: 'he',
birth: 970407,
bio: 'home',
location: 'korea'
}
let user1:{
id:number,
name: string,
nickname: string,
birth: number,
bio: string,
location: string
} = {
id: 2,
name:'tom',
nickname: 'she',
birth: 987842,
bio: 'home',
location: 'korea'
}
//Type Alias
type User = {
id:number,
name: string,
nickname: string,
birth: number,
bio: string,
location: string
}
let user:User = {
id: 1,
name:'mike',
nickname: 'he',
birth: 970407,
bio: 'home',
location: 'korea'
}
let user1:User = {
id: 2,
name:'tom',
nickname: 'she',
birth: 987842,
bio: 'home',
location: 'korea'
}
Index Signature(인덱스 시그니처)
규칙을 갖고 움직이는 객체의 타입을 정의할 때 사용하는 방식이다. 객체 내부의 속성과 속성값의 타입이 굉장히 많은 경우에 객체에 포함된 모든 속성과 속성값의 타입이 고정적이지만 동일한 타입을 무수히 많이 정의하는 것은 효율적이지 못하다. 그런 경우에 Index Signature 방식을 사용하여 객체 속성의 타입을 정의해주면 된다. 사용 예시는 아래와 같다.
type CountryCodes = {
[key:string] : string
}
let countryCodes:CountryCodes = {
korea: 'ko',
usa: 'us',
japan: 'jp'
}
let countryCodes1:CountryCodes = {}; // Not Error
위의 예시에서 속성명인 국가 이름과 속성값인 국가 코드는 모두 string 형태의 변수이며 추가되는 속성들도 모두 같을 것이기 때문에 국가 별로 일일이 작성하는 것이 아닌 Index Signature 방식을 이용해서 효율적으로 타입을 정의하는 것이다.
Index Signature는 타입의 형태만 정의하는 것이기 때문에 객체 내부가 비어있어도 오류를 발생시키지 않는다.
수많은 속성중에서도 필수로 포함되어야 하는 속성이 있을 수 있다. 그런 속성은 아래와 같이 Type Alias에 별도로 분리하여 정의해주면 된다. 이러한 경우에는 객체 내부가 비어있으면 오류를 발생시킨다.
type CountryNumbers = {
[key:string]: number,
korea: number // 필수로 포함되어야하는 property를 작성하는 방법
}
let countryNumbers:CountryNumbers = {
korea: '82',
usa: '00',
japan: '01'
}
let countryNumbers1:CountryNumbers = {}; //Error
let countryNumbers:CountryNumbers = {
usa: '00',
japan: '01'
} // Error
하지만 필수로 포함되어야 하는 속성의 Type이 Index Signature로 정의된 Type과 일치하지 않는 경우는 오류가 발생한다. 따라서 Type은 일치거나 호환되도록 정의해주도록 주의해야한다.
(객체 타입의 호환에 대해서는 다음 글에서 다룰 예정이다.)
type CountryNumbers = {
[key:string]: number,
korea: string // Error
}
[Enum Type]
Enum Type
- 여러가지 값들에 각각 이름을 부여해 열거해두고 사용하는 타입
<숫자형 Enum>
enum 타입에 특정 명칭으로 정의를 해두면 enum 타입 내부의 값들은 자동으로 0부터 번호가 매겨지게 된다.
특정 값에 임의의 번호를 부여해주면 그 다음 값들은 순차적으로 값이 매겨지게 된다. 아래에 예시를 보자.
enum Role {
A,
B,
C = 10,
D
}
const user1 = {
name: "A",
role: Role.A
}
const user2 = {
name: "B",
role: Role.B
}
const user3 = {
name: "C",
role: Role.C
}
const user4 = {
name: "D",
role: Role.D
}
console.log(user1); //{name: "A", role: 0}
console.log(user2); //{name: "B", role: 1}
console.log(user3); //{name: "C", role: 10}
console.log(user4); //{name: "D", role: 11}
<특정 값 Enum>
enum 타입에 숫자 값이 아닌 다른 값을 부여하는 것도 가능하다.
enum Language {
Korea: "ko",
English: "en"
Japan: "jp"
}
const one = { lan: Language.Korea };
const two = { lan: Language.English };
const three = { lan: Language.Japan };
console.log(one.lan); // "ko"
console.log(two.lan); // "en"
console.log(three.lan); // "jp"
앞서 보았던 내용들에 따르면 Type Alias와 같이 별도로 정의해둔 타입들은 TS 파일을 JS 파일로 컴파일하는 과정에서 사라진다고 알고 있기에 Enum Type의 값을 어떻게 사용해도 오류가 나지 않는지에 대한 의문을 가질 수 있다. Type Alias를 비롯하여 변수에 정의한 타입들은 컴파일 과정에서 사라져 JS 파일에 나타나지 않지만, Enum Type은 컴파일 과정을 거쳐도 JS 파일에서 사라지지 않고 객체 형태로 변환이 된다. 따라서 객체의 속성값을 사용하듯이 Enum Type에 정의된 값을 사용할 수 있는 것이다.
[Any와 Unknown Type]
Any Type
- 특정 변수의 타입을 확실이 모를 때 사용하는 타입이며 모든 타입과 호환이 가능하지만 굉장히 위험한 타입이다.
Any Type은 모든 타입의 슈퍼 타입이자 서브 타입이 가능하기에 치트키와 같아서 타입 검사를 안하는 것과 다를 바가 없어서 런타임 오류가 발생할 확률이 높은 타입이다.
let anyVar: any = 10;
let num:number = 10;
anyVar = "hello"
anyVar = true;
anyVar = () => {}
num = anyVar; //Not Error
anyVar.toUpperCase(); //Not a Syntax Error, Runtime Error
위의 예시를 보면 anyVar이라는 변수에 모든 타입의 변수가 할당이 가능하여 어떤 값을 넣거나 어떤 method를 사용해도 문법적 오류는 발생하지 않는다. 하지만 해당 파일을 실행하였을 경우 런타임 에러가 발생한다.
따라서 사용하는 것을 지양하는 것이 좋다.
Unknown Type
- Any Type과 같이 모든 타입으로 사용이 가능하여 모든 타입의 값을 할당 받을 수 있지만 모든 타입에 할당을 할 수는 없다.
(타입 좁히기라는 방식을 통하여 다른 타입의 변수에 할당하여 사용할 수는 있지만 타입 좁히기에 관해서는 다음에 다룰 예정이다.)
[Void와 Never Type]
Void Type
- 공허, 빈 것을 의미하는 단어로 아무것도 없음을 의미하는 타입이다.
Void Type으로 정의한 변수에는 undefined와 엄격한 null 값 검사 옵션이 false인 경우에는 null까지만 할당이 가능하다.
아무것도 없는 타입이므로 변수를 할당하는 것이 불가능한 것이다. 이 뿐만 아니라 함수의 return 값의 타입을 정의하는데, 함수의 return 값이 없는 경우에 Void Type을 사용한다.
우리는 JS에서 아무것도 없는 값에 대해서는 undefined와 null을 사용한다고 알고 있는데 Void Type이 필요한 이유는 무엇일까?
TS에서 함수의 return 값에 대한 타입을 undefined나 null 타입으로 정의를 해버리면 그 함수는 return 값이 없어도 되는 것이 아닌 undefined나 null 값 자체만을 반환해야한다. 반환 값이 없거나 모를 때 사용이 불가능하기 때문에 Void Type을 사용하는 것이다.
Never Type
- 존재하지 않는, 불가능한 타입을 의미한다.
Never Type은 타입 계층도에서 보았듯이 모든 타입의 서브 타입이기 때문에 그 어떤 타입 및 값도 할당받을 수 없다. 심지어 null 타입과 any 타입까지 모두 불가능하다. Never Type은 자체로 독립적인 타입이다.
이런 Never Type은 우리가 코드를 작성할 때 에러를 반환하는 함수나, 종료 자체가 불가능한 예를 들어 값의 반환이 없는 특정 반복과 같은 단순 기능만을 하는 함수의 return 값의 타입으로 사용된다.
function Loop():never {
while(true){
...
}
}
function Error():never {
throw new Error();
}