타입스크립트를 34.3% 정도만 알고있는 나로서 기초부터 정리할 필요가 있다고 느꼈다. 아는만큼 보인다고 했던가. 내가 아는 interface와 types 및 primitive type만 쓰니까 타입스크립트의 효용 가치를 100% 느끼지 못한다고 생각이 들어 타입스크립트 document를 읽고 있다. 보면서 엇 이건 중요해! 하는 건 한번 정리해보려 한다.
우리에겐 Javascript 라는 강력한 생태계가 있는데, 왜 Typescript가 부상했을까?
가장 큰 이유는 Typescript는 정적타입 언어로서 컴파일 과정에서 오류를 잡아낼 수 있다는 점 때문이다. Javascript의 버그 중 15%를 Typescript를 사용하면 예방할 수 있다는 연구가 있을 정도로 타입을 선언해주지 않아서 발생하는 Javascript의 예상치 못한 버그의 비율이 꽤 높다. 그리고 type을 지정해줌으로서 코드가 직관적이라는 점 또한 장점이다.
이외에도 Typescript는 ES6 이상의 문법들을 ES5(또는 ES3)로 바꿔주기 때문에 크로스 브라우징 문제를 해결한다. _(without babel..✂️)_
Typescript를 왜 쓰는지, 간략하게 살펴봤으니 Typescript의 독특한 Type들에 대해 알아보자!!
primitive type은 number, string, boolean 이 있다. 기존 javascript에서 사용하는 type과 크게 다른 부분이 없고, 선언하는 방식이 (살짝) 다르다.
// 명시적으로 number type 설정
let id:number = 1;
// 명시적으로 string type 설정
let name:string = 'eunivverse';
// 명시적으로 boolean type 설정
let isSolo:boolean = false;
//////////////// !!! 오류 발생 !!! ////////////////
product_id = 'p2';
// [오류] => [ts] '"p2"' 형식은 'number' 형식에 할당할 수 없습니다.
// let product_id: number
Array 는 Array<string> 혹은 string[]으로 type을 선언하여 사용할 수 있다. ([string] 과는 다르다. 이건 나중에 알아보도록 하자)
바로 코드를 통해 알아보자!
// 오직 숫자 아이템만 허용
let members = ['이승협', '유회승'];
// [오류]
// [ts] 'number[]' 형식은 'string[]' 형식에 할당할 수 없습니다.
// 'number' 형식은 'string' 형식에 할당할 수 없습니다.
// let members: string[]
members = [1, 2, 3];
let nums:number[] = [100, 101, 102];
// 오직 string 아이템만 허용
let strs:string[] = ['ㄱ', 'ㄴ', 'ㄷ'];
// 오직 boolean 아이템만 허용
let boos:boolean[] = [true, false, true];
// 모든 데이터 타입을 아이템으로 허용
let anys:any[] = [100, 'ㄴ', true];
// 특정 데이터 타입만 아이템으로 허용 (number|string)
let selects:(number|string)[] = [102, 'ㅇ'];
any는 typescript에만 있는 type이다. 명시적으로 type을 지정해 사용하는 typescript에서 어떤 type을 할당해야할지 모르겠을 때, 사용한다.
// 명시적으로 any 타입 지정
let id:any = 124981;
// any 유형이 설정되었으므로 어떤 유형도 값으로 할당 가능
id = 'p9023412';
// 암시적으로 any 타입 지정
let id;
id = 124981;
id = 'p9023412';
noImplicitAny 는 변수들이 미리 정의된 타입을 가져야 하는지 여부를 제어하는 설정이다.
noImplicitAny가 설정되었다면 type을 선언하지 않은 데이터들이 any type으로 간주되지 않기 때문에 아래 코드는 오류를 발생시킨다.
function add(a, b) {
// ~'a'매개변수는 암시적으로 'any' 형식이 포함됩니다
// ~'b'매개변수는 암시적으로 'any' 형식이 포함됩니다
return a+b;
}
tuple도 typescript에만 있는 type이다. 배열과는 달리 명시적으로 지정된 형식에 따라 아이템을 설정해야한다.
let eunivverse:[string, number] = ['eunivverse', 27];
// [오류]
// [ts] '[number, string]' 형식은 '[string, number]' 형식에 할당할 수 없습니다.
// 'number' 형식은 'string' 형식에 할당할 수 없습니다.
// let eunivverse:[string, number]
eunivverse = [28, 'eunivverse'];
// [오류]
// [ts] 'false' 형식의 인수는 'string | number' 형식의 매개 변수에 할당될 수 없습니다.
eunivverse.push(false);
// named tuple 선언
const graph : [x : number , y : number , pointName : string] = [12.3 , 32.1 , "A"];
enum은 열거형 데이터 타입이다. 열거된 멤버는 별도의 값이 설정되지 않은 경우 0부터 시작한다.
enum Team {
Manager = 101,
Planner = 208,
Developer = 302,
Designer, // 302 + 1
}
let yamoo9:number = Team.Manager; // (enum member) Team.Manager = 101
let sarha:number = Team.Designer; // (enum member) Team.Designer = 303
enum은 js코드로 컴파일 되는데, enum 데이터 코드는 멤버는 숫자 또는 데이터 값을 속성으로 하는 객체를 생성하는 코드로 변환된다. 변환된 코드는 다음과 같다.
var Team;
(function (Team) {
Team[Team["Manager"] = 101] = "Manager";
Team[Team["Planner"] = 208] = "Planner";
Team[Team["Developer"] = 302] = "Developer";
Team[Team["Designer"] = 303] = "Designer";
})(Team || (Team = {}));
var yamoo9 = Team.Manager;
var sarha = Team.Designer;
(하지만 enum의 여론이 좋지않다. union 타입을 사용하는 것을 추천한다.)
=> 간략하게 이유를 설명하면, typescript에서 enum은 javascript로 변환한 후, 트랜스파일링을 거치면 IIFE(즉시실행함수)로 생성이 된다. 그러나 번들러는 IIFE(즉시실행함수)를 '사용하지 않는 코드'로 판단할 수 없어 tree shaking이 되지 않기 때문에 실제로 사용하지 않는 코드라도 최종 번들에는 포함되는 문제가 발생한다.
변수에 설정 가능한 타입이 여러개일 때, |(파이프)를 사용하여 설정한다.
function setInfo(id:number|string, name:string){
return { id, name };
}
intersection type은 여러 type 을 모두 만족하는 type이다.
interface Person {
name: string;
gender: boolean;
}
interface Student {
name: string;
age: number;
}
// Intersection Type 선언!!!
type Eunivverse = Person & Student;
/***
Eunivverse 구조
{
name: string;
age: number;
gender: boolean;
}
***/
typescript에서 함수를 선언할 때, 함수의 입력과 출력 타입을 지정한다. void 는 결과 값을 반환하지 않을 때 사용한다.
익명 함수의 경우, 함수 선언과는 조금 다르다. 함수가 코드상에서 위치한 곳을 보고 해당 함수가 어떻게 호출될지 알아낼 수 있다면, TypeScript는 해당 함수의 매개 변수에 자동으로 타입을 부여한다.
function getFavoriteNumber(): number {
return 26;
}
// 변수에 함수 매개변수, 리턴 타입에 대한 명시적 설정
let assignClass: (n:string) => void;
/////////// 익명 함수
const names = ["Alice", "Bob", "Eve"];
// 함수에 대한 문맥적 타입 부여
// Property 'toUppercase' does not exist on type 'string'. Did you mean 'toUpperCase'?
names.forEach(function (s) {
console.log(s.toUppercase());
});
// 화살표 함수에도 문맥적 타입 부여는 적용된다.
// Property 'toUppercase' does not exist on type 'string'. Did you mean 'toUpperCase'?
names.forEach((s) => {
console.log(s.toUppercase());
});
변수에 초기 설정된 값을 암시적으로 할당 가능한 데이터 타입이다. 초기 설정된 값과 다른 형태로 할당하면 오류를 출력한다.
let Dom: {version:string, el:()=>void, css:()=>void};
Dom = {
version: '0.0.1',
el(){},
css(){}
};
// Dom에 없는 함수를 추가하려면 오류 발생!!!
// [오류]
// [ts] '{ version: string; el: () => void; css: () => void; }' 형식에 'each' 속성이 없습니다.
// any
Dom.each = function(){};
// 위와 같은 문제를 해결하기 위한 방법
let Dom: {
version: string,
el: () => void,
css: () => void,
[propName: string]: any // ⬅︎
};
Dom = {
version: '0.0.1',
el(){},
css(){}
};
Dom.each = function(){};
Dom.map = function(){};
Dom.filter = function(){};
두가지 type을 코드로 비교해보자!!
////////////////// object type //////////////////
// object라는 type으로 `user` 선언
let user: object = {
id: 1,
name: 'eunivverse',
};
// [ERROR] 점 표기법으로 접근시 에러 발생
user.id
////////////////// object literal type //////////////////
let user1: {
id: number,
name: string
} = {
id: 1,
name: 'eunivverse'
};
// 위와 동일한 형태로 선언하는 방식이다. (type 생성 -> type과 맞는 변수 선언)
type userType = {
id: number,
name: string
}
let user2: userType = {
id: 1,
name: 'eunivverse'
}
// 점 표기법으로 user1객체의 id 값을 바꿀 수 있다.
user1.id = 2
console.log(user1) // { id: 2, name: 'eunivverse' }
// id를 외부에서 수정할 수 없게 하고 싶다면 앞에 readonly를 불이면 된다. (Readonly Property)
// name이 필수요소가 아니라면 ?를 붙이면 된다. (Optional Property)
type user1 = {
readonly id: number,
name?: string
}
// name이 없지만 오류가 발생하지 않으나, 외부에서 id를 수정할려고 해서 에러가 발생한다.
let user1: userType = {
id: 1,
}
// 에러 발생
user1.id = 2
빈 값 또는 초기화되지 않는 값을 가리키는 원시값이지만, typescript에서는 type으로 존재한다. strictNullChecks 설정 여부에 따라 동작 방식이 달라진다. 코드를 통해 알아보자!
// strictNullChecks 가 해제되어있을 때
// 문제없음
const x:number = null;
// strictNullChecks 가 설정되어있을 때
// [ERROR]
// ~'null'형식은 'number' 형식에 해당할 수 없습니다.
const x:number = null;
// 오류(null, undefined)가 검출되어 null을 허용하려고 한다면, type으로 선언하여 오류를 해결할 수 있다.
const x:number| null = null;
구제적인 문자열과 숫자를 type 위치에 지정할 수 있다.
// type 위치에 'hello' 라는 문자열로 선언했다.
let x: "hello" = "hello";
// 문제없음
x = "hello";
// [ERROR] ===> hello가 아니기 때문!!!!
x = "howdy";
// 함수의 매개변수 혹은 return 값에도 사용이 가능하다.
// alignment 는 left/right/center 중에 설정이 가능하다.
function printText(s: string, alignment: "left" | "right" | "center") {
// ...
}
// 문제없음
printText("Hello, world", "left");
//[ERROR] ===> left/right/center 중에 없기 때문!!!
printText("G'day, mate", "centre");
// type 혹은 interface로 선언한 값과 같이 사용이 가능하다.
interface Options {
width: number;
}
// 문제없음
function configure(x: Options | "auto") {
// ...
}
// return 값에서도 literal type이 사용된다.
// 문제없음
function compare(a: string, b: string): -1 | 0 | 1 {
return a === b ? 0 : a > b ? 1 : -1;
}
typescript를 쓰다보면, Type string is not assignable to type "~~" 라는 오류를 볼 수 있다.
function readBook(name: 'eunivverse') { }
const book = {
bookName: 'eunivverse'
};
// Error: Argument of type string is not assignable to parameter of type 'name'
readBook(book.bookName);
book.bookName은 eunivverse인데도 불구하고, 오류가 나는 이유는 무엇일까??
이것은 book의 bookName은 string 타입이라고 추론되었기 때문이다.고치는 방법은 아래와 같다.
// 해결방법
// type을 표명해서 TypeScript에게 어떤 리터럴 타입으로 추론되어야 하는지 알려준다.
// 1번 해결방법
function readBook(name: 'eunivverse') { }
const book = {
bookName: 'eunivverse' as 'eunivverse'
};
// 문제없음
readBook(book.bookName);
// 2번 해결방법
function readBook(name: 'eunivverse') { }
type Book = {
bookName: 'eunivverse',
}
// bookName의 추론된 타입은 항상 'eunivverse' 이다. => type을 붙인다.
const book: Book = {
bookName: 'eunivverse'
};
// 문제없음
readBook(book.bookName);
위의 예시 코드에도 나온 type, interface 등 여러 개의 타입이 있다. 그 중 type 과 interface 는 자주 사용되는데, 이전에 정리한 글이 있어서 해당 게시글을 참고하면 될 것 같다!!
[type과 interface 비교]
https://velog.io/@eunivverse/TypeScript-type-VS-interface