이전에 타입스크립트를 간단히 배우고 리액트에 적용을 해보았는데, 타입스크립트의 문법을 조금 더 자세하게 알아보자.
타입스크립트는 강력한 타입으로 대규모 애플리케이션 개발에 용이하며 유명한 자바스크립트 라이브러리와의 편리한 사용이 가능하다. 또한 개발 도구에서 강력한 지원이 가능하다는 장점이 있다.
이전에 말했듯이 타입스크립트를 이용하면 많은 실수를 줄일 수 있다라는 장점이 있다!
yarn add typescript -g
다음과 같이 타입스크립트를 전역적으로 설치해주자.
//01_hello.ts
var hello = "hello";
let hello2 = "hello2";
다음과같은 간단한 예제코드를 작성하고
tsc .\01_hello.ts
터미널에서 tsc파일로 바꾸어주면 01_hello.js파일이 생성된 것을 알 수 있다.
//01_hello.js
var hello = "hello";
var hello2 = "hello2";
typescript는 구형 브라우저도 지원하기 때문에 var로 바뀌어서 나오게 된다.
만약 es6의 문법을 그대로 들고가고 싶다면
tsc .\01_hello.js --target es6
다음 명령어를 사용하면 된다.
//01_hello.ts
var hello = "hello";
let hello2 = "hello2";
let timeoutPromise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("1 sec");
}, 1000);
});
timeoutPromise.then(console.log);
다음과같이 Promise를 사용하려고 하면
PS C:\Users\user\Desktop\Git\front_end_study\8_TypeScript> tsc .\01_hello.ts
01_hello.ts:4:26 - error TS2585: 'Promise' only refers to a type, but is being used as a value here. Do you need to change your target library? Try changing the 'lib' compiler option to es2015 or later.
4 let timeoutPromise = new Promise((resolve, reject) => {
~~~~~~~
Found 1 error.
위와같은 오류가 뜨는데, 이를 해결하려면 라이브러리 옵션을 주면 된다.
tsc .\01_hello.ts --lib es5,es2015.promise,es2015.iterable,dom
물론 구현할때마다 일일히 터미널에 칠 필요는 없다.
tsconfig.json
위 이름의 파일을 하나 생성해주자.
{
"include": ["src/**/*.ts"],
"exclude": [
"node_modules" //node 패키지들은 컴파일 제외
],
"compilerOptions": {
"module": "commonjs",
"rootDir": "src",
"outDir": "dist",
"target": "ES5",
"sourceMap": true, // typescript를 개발자옵션에서 확인가능
"noImplicitAny": true //타입을 정해야지만 가능하게
}
}
방금 옵션으로 넣어주었던 내용들을 위와같이 파일로 미리 저장해둬서 사용할 수 있다.
이제 터미널에서
tsc
만 입력해도
파일이 잘 생성된것을 알 수 있다.
변수선언
var : var키워드는 함수단위 블록스코프이다.
const : 블록스코프를 가지게 된다.
const의 경우 변수를 설정하면 타입이 정해지기 때문에
function outer() {
let score = 0;
score = 30;
score = '30' // error
}
값이 정해지면 타입이 정해지게 된다.
function outer() {
let score;
score = 30;
score = '30'
}
하지만 값이 주어지지 않으면 type이 any가 되기 때문에 에러가 발생하지 않는다.
function outer() {
let score : number;
score = 30;
score = '30' // error
}
: number로 타입을 지정해줄 수도 있다.
var와 let의 차이를 보려면 밑의 코드를 한번 보자
for ( let i =0; i< 3; i++) {
setTimeout(function() {
console.log(i); //0,1,2
},100)
}
for ( var i =0; i< 3; i++) {
setTimeout(function() {
console.log(i); // 3 3 3
},100)
}
비슷해 보이는 코드이지만, 출력결과가 다르다.
이는 var는 함수단위 스코프이기 때문이다.
const
const는 상수형으로 값을 변경할 수 없다.
function outer() {
const score : number = 100;
score = 30; //error
}
이때 굳이 :number를 사용하지 않아도 값을 넣음으로서 타입이 정해지기 때문에 생략해도 무방하다.
타입스크립트의 기본타입
number
string
boolean
undefined
null
5개의 자바스크립트 기본타입과
object
객체와
symbol
es5부터 생긴 총 7개의 기본타입을 지원한다.
undefined와 null은 모든 타입의 상위타입이다.
let numValue : number;
numValue = null;
위와같이 덮어씌우는 식으로 값을 넣어주는것이 가능하다.
symbol
symbol이라는 함수타입으로만 생성이 가능하다.
배열
배열같은 경우는 아래와 같은 방식으로 선언 가능하다.
let nameList : string[]
string말고 다른 타입도 다 가능하다!
객체를 inline타입으로 정해줄 수 있다.
let user1 : {name : string, score : number };
user1 = {
name : 'mingyu',
score : 30
}
위처럼 타입을 저장해놓으면 그외에는 사용할 수 없다.
let user1 : {name : string, score : number };
user1 = {
name : 'mingyu',
score : 30,
min : 20 //error
}
튜플
튜플은 배열과 유사하다
let tuple2 : [number, string];
tuple2 = [] //error
tuple2 = [1,2] //error
tuple2 = [1, 'mingyu']
tuple2 = [1,'mingyu',2] //error
튜플안의 개수와 key의 자료형이 모두 맞아야지만 사용이 가능하다.
interface
기본 자료형이 아닌, 하나의 타입을 만드는 방법이다.
interface TV {
turnOn() : boolean; //return 값 설정
turnOff() : void;
}
const myTV: TV = {
turnOn(){
return true;
},
turnOff(){
}
};
function tryTurnOn(tv:TV) {
tv.turnOn();
}
tryTurnOn(myTV)
다음과 같이 사용할 수 있다.
인터페이스를 잘 이용하면 깔끔한 코드를 만들기 쉽다.
예를들어 보드를 만드는 코드를 한번 보자.
interface Cell {
row: number;
col: number;
piece?: Piece; //Piece는 있을수도있고 없을수도있다.
}
interface Piece {
move(from: Cell, to: Cell): boolean;
}
function createBoard() {
const cells: Cell[] = [];
for (let row = 0; row < 4; row++) {
for (let col = 0; col < 3; col++) {
cells.push({
row,
col,
});
}
}
return cells;
}
const board = createBoard();
board[0].piece = {
move(from: Cell, to: Cell) {
return true;
},
};
중요한건 타입을 지정해놓으면 함수의 Props로 넘겨주거나 할때 그 타입을 꼭 지켜야지만 정상동작하게 된다는 것이다.
함수형 타입
function add (x : number, y : number) : number {
return x + y;
}
//add(1, "2") //error
Props뿐 아니라 반환하는 형도 type을 지정해줘야한다. 명시적으로 표시하지 않으면 추정해서 넣어지게 된다.
function add(x: number, y: number): number {
return x + y;
}
//add(1, "2") //error
function buildUserInfo(name = "-", email?: string) {
return { name, email }; // name : name, email : email
}
const user = buildUserInfo(); //-, undefind
const user2 = buildUserInfo("mingyu"); // mingyu, undefind
방금 배운 ?와 초기값을 설정해서도 사용할 수 있다.
const add2 = ( a: number, b:number) => a+b;
화살표 함수를 써도 잘 적용된다!
함수의 타입과 반환형을 미리 표현해놓는 오버로드 시그니처를 이용할수도 있다.
interface Storage {
a: string;
}
interface ColdStorage {
b: string;
}
//함수 오버로드 시그니처
function store(type: "통조림"): Storage;
function store(type: "아이스크림"): ColdStorage;
function store(type: "통조림" | "아이스크림") {
if (type === "통조림") {
return { a: "통조림" };
} else if (type === "아이스크림") {
return { b: "아이스크림" };
} else {
throw new Error("에러 발생!!");
}
}
const s = store("아이스크림");
console.log(s.b); // 아이스크림
다음과 같이 오버로드 시그니처를 미리 만들어둔 후에, store를 실제로 불러내서 사용할 수 있다.
type으로 위와같이 문자열을 지정해두면, 지정한 문자만 입력으로 받을 수 있게된다.
enum
열거형이다.
enum StarbuksGrade {
WELCOME,
GREEN,
GOLD,
}
function getDiscount(v: StarbuksGrade): number {
switch (v) {
case StarbuksGrade.WELCOME:
return 0;
case StarbuksGrade.GREEN:
return 5;
case StarbuksGrade.GOLD:
return 10;
}
}
console.log(getDiscount(StarbuksGrade.GREEN)); // 5
console.log(StarbuksGrade.GREEN); //1
console.log(StarbuksGrade);
// {
// '0': 'WELCOME',
// '1': 'GREEN',
// '2': 'GOLD',
// WELCOME: 0,
// GREEN: 1,
// GOLD: 2
// }
console.log(StarbuksGrade["0"]) //WELCOME
다음과같이 사용할 수 있다. 이때 만약 WELCOME과 GREEN사이에 값이 넣어지면 함수가 다 꼬여질 수도 있다.
enum StarbuksGrade {
WELCOME = 0,
GREEN = 1,
GOLD = 2,
}
다음과같이 순서를 미리 지정해두면 이를 막을 수 있다.
enum StarbuksGrade {
WELCOME = "WELCOME",
GREEN = "GREEN",
GOLD = "GOLD",
}
function getDiscount(v: StarbuksGrade): number {
switch (v) {
case StarbuksGrade.WELCOME:
return 0;
case StarbuksGrade.GREEN:
return 5;
case StarbuksGrade.GOLD:
return 10;
}
}
console.log(getDiscount(StarbuksGrade.GREEN)); // 5
console.log(StarbuksGrade.GREEN); //GREEN
console.log(StarbuksGrade); //{ WELCOME: 'WELCOME', GREEN: 'GREEN', GOLD: 'GOLD' }
위처럼 숫자말고 문자열로 지정해둘수도 있다! 자주 사용되는 방식이다.
클래스
클래스도 당연히 타입을 지정해가면서 사용할 수 있다.
interface User {
name: String;
}
interface Product {
id: string;
price: number;
}
class Cart {
protected user: User;
private store: object;
constructor(user: User) {
this.user = user;
this.store = { id: "-", price: 0 };
}
put(id: string, product: Product) {
this.store = product;
}
get(id: string) {
return this.store;
}
}
const cartMingyu = new Cart({ name: "mingyu" });
//console.log(cartMingyu.store); //error private이기때문
const cartJeong = new Cart({ name: "Jeong " });
상속도 가능하다. 추가적으로 말하자면 상속된 클래스도 privated엔 접근이 불가능하며 protected는 접근이 가능하다.
class PromotionCart extends Cart {
addPromotion(){
//console.log(this.store) //error
console.log(this.user)
}
}
const cart2 = new PromotionCart({ name : 'mingyu '});
cart2.addPromotion //'mingyu
아래처럼 생성자를 만들면서 private와같은 속성까지 지정하는것도 가능하다.
constructor(protected user: User) {
this.user = user;
this.store = { id: "-", price: 0 };
}
이번엔 좀 전에 배운 interface로 타입을 지정해서 클래스를 만들어보자.
interface Person {
name: string;
say(message: string): void;
}
interface Programmer {
writeCode(requirment: string): string;
}
class K_Programmer implements Person, Programmer {
//Korean은 Person을 보장
constructor(public name: string) {}
writeCode(requirment: string): string {
console.log(requirment);
return requirment + ".....";
}
say(message: string): void {
console.log(message);
}
loveKimchi() {
console.log("I like kimchi");
}
}
const mingyu = new K_Programmer("mingyu");
조금 구조가 복잡해졌지만 보면 이해하기 어렵지는 않다.
implements를 이용해서 만든 interface를 보장한다고 명시한점만 유의있게 보면 될 것 같다.
추상클래스를 활용한 조금더 복잡한 코드를 보자.
추상클래스는 오버로드 시그니처처럼 미리 타입들을 지정해둔 클래스를 의미한다. 추상클래스만 선언한 후에 불러내서 사용할 수는 없다.
interface Person {
name: string;
say(message: string): void;
}
interface Programmer {
writeCode(requirment: string): string;
}
abstract class Korean implements Person {
public abstract jumin: number;
constructor(public name: string) {}
say(message: string): void {
console.log(message);
}
abstract loveKimchi(): void;
}
class K_Programmer extends Korean implements Programmer {
//Korean은 Person을 보장
loveKimchi() {
console.log("I like kimchi");
}
constructor(public name: string, public jumin: number) {
super(name);
}
writeCode(requirment: string): string {
console.log(requirment);
return requirment + ".....";
}
say(message: string): void {
console.log(message);
}
}
const mingyu = new K_Programmer('mingyu', 1111);
//const mingyu2 = new Korean('mingyu') //error
Korean 이란 추상클래스를 만든후에, K_Programmer란 클래스에 상속을 해서 사용한 것을 알 수 있다.
제네릭
타입의 파라미터화를 하는것이다.
간단한 예시를 먼저 보자
function createPromise<T>(x: T, timeout: number) {
return new Promise((resolve: (v: T) => void, reject) => {
setTimeout(() => {
resolve(x);
}, timeout);
});
}
createPromise(1, 100) //type이 number로표시
.then((v) => console.log(v));
createPromise<string>('1', 100) //type이 string으로표시
.then((v) => console.log(v));
다음처럼 x의 타입을 T로 넣어주면 알아서 타입이 정해지는것을 알 수 있다.
function createPromise<T>(x: T, timeout: number) {
return new Promise<T>((resolve, reject) => {
setTimeout(() => {
resolve(x);
}, timeout);
});
}
Promise에도 T로 넘겨주면 굳이 resolve옆에 타입을 적지 않아주어도 된다.
타입을 파라미터화 해서 타입을 유지하면서 코드를 작성하게 하는 방식이란걸 생각하면 될 것 같다. 아래는 튜플로 반환해보는 예시이다.
function createTuple2<T, U>(v: T, v2: U): [T, U] {
//튜플로 반환
return [v, v2];
}
const t1 = createTuple2("mingyu", 25); //2개의 튜플이 저장
제네릭은 클래스를 정의할때도 사용된다.
class LocalDB {
constructor(private localStorageKey: string) {}
add(v: User) {
localStorage.setItem(this.localStorageKey, JSON.stringify(v));
}
get() :User {
const v = localStorage.getItem(this.localStorageKey);
return v ? JSON.parse(v) : null;
}
}
interface User {
name: string;
}
const userDb = new LocalDB("mingyu");
userDb.add({ name: "mingyu" });
const user1 = userDb.get();
user1.name; //type : string
위코드를 제네릭을 적용하면
class LocalDB<T> {
constructor(private localStorageKey: string) {}
add(v: T) {
localStorage.setItem(this.localStorageKey, JSON.stringify(v));
}
get() :T {
const v = localStorage.getItem(this.localStorageKey);
return v ? JSON.parse(v) : null;
}
}
interface User {
name: string;
}
const userDb = new LocalDB<User>("mingyu");
userDb.add({ name: "mingyu" });
const user1 = userDb.get();
user1.name; //type : any
이처럼 된다. 즉, 타입을 파라미터처럼 사용할수 있게 된 것이다.
interface에서도 제너릭을 사용할 수 있다.
interface DB<T>{
add(v:T) : void;
get() : T;
}
class D<T> implements DB<T> {
add(v: T): void {
throw new Error("Method not implemented.");
}
get(): T {
throw new Error("Method not implemented.");
}
}
class LocalDB<T> implements DB<T> { //<T>가 이어지면서 전달된다.
constructor(private localStorageKey: string) {}
add(v: T) {
localStorage.setItem(this.localStorageKey, JSON.stringify(v));
}
get() :T {
const v = localStorage.getItem(this.localStorageKey);
return v ? JSON.parse(v) : null;
}
}
interface User {
name: string;
}
const userDb = new LocalDB<User>("mingyu");
userDb.add({ name: "mingyu" });
const user1 = userDb.get();
user1.name; //type : any
대신 interface를 사용해서 쓸때 class이름에도 제너릭을 가져와야 사용이 가능함을 알 수 있다.
특정 type의 high-type으로써 제너릭을 사용할수도 있다. 범위를 고정시킨다고 볼수도 있을 것 같다.
interface JSONSerialier {
serialize() : string;
}
class LocalDB<T extends JSONSerialier> implements DB<T> { //<T>가 이어지면서 전달된다.
constructor(private localStorageKey: string) {}
add(v: T) {
localStorage.setItem(this.localStorageKey, JSON.stringify(v));
}
get() :T {
const v = localStorage.getItem(this.localStorageKey);
return v ? JSON.parse(v) : null;
}
}
interface User {
name: string;
}
제너릭에서 조건문 타입도 사용할 수 있다.
interface Veigtable {
v: string;
}
interface Meat {
m: string;
}
interface Cart2<T> {
getItem(): T extends Veigtable ? Veigtable : Meat;
}
// const cart1 : Cart2<string> ={ //error
// getItem() {
// return ''
// }
// }
const cart2: Cart2<string> = {
getItem() {
return { m: "" };
},
};
// const cart3 : Cart2<Veigtable> ={ //error
// getItem() {
// return { m : ""}
// }
// }
하나의 타입뿐이 아닌 여러개의 타입에 따라서 동작이 달라지는것을 확인할 수 있다.
Intersection
interface User {
name: string;
}
interface Action {
do(): void;
}
function createUserAction(u: User, a: Action): User & Action { //intersection
return { ...u, ...a };
}
const u = createUserAction({ name: "mingyu" }, { do() {} });
//굳이 UserAction이라던가 하는 interface를 만들필요가 없다!
두 인터페이스를 합쳐서 반환하는 예제이다
Union Types
function compare(x: string | number, y: string | number) {
if (typeof x === "number" && typeof y === "number") {
return x === y ? 0 : x > y ? 1 : -1;
}
if (typeof x === "string" && typeof y === "string") {
return x.localeCompare(y); //같으면 0 크면1 작으면 -1
}
throw new Error("not supported type");
}
const v = compare("a", "b");
const w = compare("a", 1); //error but 명시적으로 표시안됨
이때 w에 오류를 출력하는걸 명시적으로 표시하고 싶다면 좀전에 배운 오버로드 시그니처를 사용하면 된다!
function compare(x: string, y: string): number;
function compare(x: number, y: number): number;
function compare(x: string | number, y: string | number) {
if (typeof x === "number" && typeof y === "number") {
return x === y ? 0 : x > y ? 1 : -1;
}
if (typeof x === "string" && typeof y === "string") {
return x.localeCompare(y); //같으면 0 크면1 작으면 -1
}
throw new Error("not supported type");
}
const v = compare("a", "b");
//const w = compare("a", 1); //error
만약 interface로 만든 type이라면 typeof를 사용하여 if문을 구현할 수 없다. 아래와같이 Action타입임을 확실시하게 만들어서 코드를 짜는 방법이 있긴하다.
interface User {
name: string;
}
interface Action {
do(): void;
}
function process ( v : User | Action ) {
if((<Action>v).do) {
(<Action>v).do
}
}
별도의 타입가드를 아래와같이 정의해두면 조금 더 편리하게 쓸 수 있다.
interface User {
name: string;
}
interface Action {
do(): void;
}
function isAction(v: User | Action): v is Action {
//타입가드 정의
return (<Action>v).do !== undefined; //v안에 do가있다면 Action이다
}
function process(v: User | Action) {
if (isAction(v)) {
v.do();
} else {
console.log(v.name);
}
}
타입 별칭
타입의 이름을 바꾸거나 별칭을 붙이는데 사용된다.
interface User {
name: string;
}
interface Action {
do(): void;
}
type UserAction = User & Action;
function createUserAction() :UserAction {
return {
do() {},
name : ''
}
}
type StringOrNumber = string | number;
type Arr<T> = T[];
type P<T> = Promise<T>;
물론 interface처럼 바로 선언하는 것도 가능하다.
type User2 = {
name: string;
login(): Boolean;
};
class UserImpl implements User2 {
name: string;
login(): boolean {
throw new Error("에러 발생!");
}
}
type을 사용하면 문자열을 type으로 바로 사용할때 유용하다.
type User2 = {
name: string;
login(): Boolean;
};
class UserImpl implements User2 {
name: string;
login(): boolean {
throw new Error("에러 발생!");
}
}
type UserState = "PENDING" | "APPROVED" | "REJECTED";
function checkUser(user: User2): UserState {
if (user.login()) {
return "APPROVED";
} else {
return "REJECTED";
}
}
UserState type을 손쉽게 구현한것을 볼 수 있다.
인덱스타입
속성의 이름이 정의되어 있지않고 동적일때 사용가능하다
interface Props {
[key: string]: string;
// [key : boolean] : string ; //error 인덱스타입은 number나 string만
name: string;
}
const p: Props = {
name: "mingyu", //필수
a: "d",
b: "e",
//c : 3 //error
1: "b", //error가 나지 않음! p[1]로 조회가능
};
어떤 값이 오든 동적으로 처리할 수 있다.
keyof Props를 사용해서 key의 타입을 가져오는것도 가능하다.
let keys : keyof Props;
interface User {
name : string;
age : number;
hello(msg : string) : void;
}
let keysofUser : keyof User;
keysofUser ="age"
let helloMethod : User["hello"]
helloMethod = function (msg : string) {
}
// helloMethod = function (msg : number) { //error
// }
이렇게 타입스크립트 언어의 문법을 조금 더 자세히 알아보았다
'FrontEnd > JavaScript' 카테고리의 다른 글
[JS] 폴리-필(Polyfill) , 바벨 (0) | 2023.05.26 |
---|---|
[JS] 정수로 만들어주는 3가지 방법 비교 (0) | 2023.05.09 |
24_타입스크립트 문법 (0) | 2022.01.06 |
14_리액트_클래스형 컴포넌트 (0) | 2021.12.24 |
08_HTML과 JS연동하기 (0) | 2021.12.20 |