타입스크립트

떡ol·2024년 6월 6일
post-thumbnail

TypeScript 목적

TypeScript라는 이름답게 정적 타입을 명시할 수 있다는 것이 순수한 자바스크립트와의 가장 큰 차이점이다. 덕분에 개발 도구(IDE나 컴파일러 등)에게 개발자가 의도한 변수나 함수 등의 목적을 더욱 명확하게 전달할 수 있고, 그렇게 전달된 정보를 기반으로 코드 자동 완성이나 잘못된 변수/함수 사용에 대한 에러 알림같은 풍부한 피드백을 받을 수 있게 되므로 순수 자바스크립트에 비해 어마어마한 생산성 향상을 꾀할 수 있다. 즉, '자바스크립트를 실제로 사용하기 전에 있을만한 타입 에러들을 미리 잡는 것' 이 타입스크립트의 사용 목적이다.

기본타입 선언

기본 초급 타입선언

// 자동추론
const a = '5'; // string
const b = 5; // number
const c = true; // boolean
const d = undefined // undefined
const e = null // null
const x: {} = 'hello' // object가 아니다 any타입
const y: Object = 'hello' // object가 아니다 any타입
const z: object = 'hello' // 객체 타입은 이렇게 생성한다.

const f: boolean = true // 타입선언하기. 하지만, 자동으로 추론되는 타입을 굳이 써줄 필요는 없다.

배열,객체 중급 타입선언

배열을 선언시에는 tuple 예시를 주의깊게 보자. .push와같은 매서드를 이용한 삽입에는 타입방어가 안된다.

const arr: string[] = ['가','나','다'];
const arr2: [number, number, string] = [123,456, 'hello'];
const tuple: [string, number] = ['1',1];

tuple[2] = 'hello' // 이건 에러가 나타나지만,
tuple.push('hello'); // 이건 에러가 없다

아래와 같이 숫자와 스트링이 섞인것은 any 타입이 나올 수도, (number | string) 타입이 될 수도 있다. 따라서 정확한 타입을 원한다면, 선언을 해준다.

// number로 하였기때문에 해당 객체는 더이상 any가 아닌 number가 적용되어야한다.
const obj:{lat: number, lon: number} = {lat:1234, lon:345.4};

//선언한 타입을 변환가능하긴하다..
let aa =123;
aa = 'hello' as unknown as number; // unknown으로 바꾼 후 number로 바꾼다.

함수, 제너릭, 클래스 고급 타입선언

다음과같이 선언이 가능하다.

type Add = () => number; // 함수 타입=> 결과는 number
interface Minus{} // 인터페이스 아래에서 더 자세히 설명
Array<string> // 제너릭

타입스크립트에서는 함수를 타입선언, 기능선언 둘로 나눠서 해줄 수 있다

fuction add(x: number, y: number): number; // 선언만, javascript에서는 이런문구는 없다.
function add(x, y) { // 실제동작부
  return x + y;
}

빈배열에는 nerver 타입이 생긴다.

  const array = [];  //const arr: string[] = []; 이게 맞는 표현
  array.push('hello'); // 에러가 난다. never타입이므로...

위에서도 예시가있지만 또는 으로 두가지 타입이 겹처올 수있는상황에서는 !를 선언하여 null, undefined를 방지할 수 있다. (권장은 안함)

  // 원래 Element | null 인데 Element로 선언이 됨, null일 수는 없다 라는 것을 뜻함.
  const head = document.querySelector('#head')!; // not null

하지만 사용 권장을 안하므로 (너무 가독성이 없음 ! 모르는사람도 있을테고...)그냥 아래와 같이 구성하자.

  if(head){
   // 로직
  }

CLASS 타입

  class A{
  	aaa () {}
  }
  class B{
  	 bbb() {}
  }
  
  //class 는 그 자체로 타입이 가능하다.
  type a = A;
  
  //그래서 변수에 선언하고 사용할때는 다음과 같이 사용한다.
  const b: a = new A();
  
  //진짜 타입만을 원할때는 typeof 를 사용한다.
  const c: typeof A = A;
  
  
  funtion aOrB(param: A | B){
  	if (param instanceof A)
  		pram.aaa();
  	else param.bbb();
  }
  
  aOrB(new A()); // class 는 인스턴트를 생성해서 선언해야한다.

Type, Enum, Interface 타입

Type 타입

type 타입은 다음과 같이 사용이 가능하다.

  type World = "world";
  const a: World = 'world'; // World타입이니 world string으로 밖에 입력 안된다.
  type Greeting = `hello ${World}`;
  const c: Greeting = 'hell'; // hello world 가 된다.

Enum 타입

enum은 자바에서 enum이라고 생각하면 된다.

  const enum Direction{
  	Up,
  	Down,
  	Left,
  	Right
  }

  const a = Direction.Up // 0;
  const b = Direction.Down //1;

결국 위의 enum은 다음과 같다.

const Direction = {
	Up: 0,
	Down: 1,
	Left: 2,
	Right: 3,
} as const;

하지만 as const 를 빼면 안됨. 빼면 타입을 추론하는 단계에서 결국

    Up: number,
  	Down: number,
  	Left: number,
  	Right: number,

이런 뜻이 되어버림, 우리가 원하는 것은 상수로서의 의미이므로 as const를 붙힌다.

typeof , keyof

keyof는 객체 타입의 키를 타입으로 변환합니다.
typeof는 변수나 객체의 타입을 추론합니다.

const obj= {a: '123', b: 'hello', c: 'world'};
typeof obj // 타입으로 선언이 되었음. 결과는 {a: string; b: string; c: string;}
type Key = keyof typeof obj; // 그리고 이 키들을 타입으로 만들겠다. 결과는 'a'|'b'|'c'

const arr= ['123', 'hello', 'world'] ;
type Key2 = keyof typeof arr; // number | "length" | "toString" | "push" | "pop" | "concat"  등등....

union |, intersection &

또는, 그리고 를 나타내는 연산

  type str = sring | null; // string or null 가능
  // and연산은 타입에서 쓰기 애매하다. 그래도 객체타입에는 써보는것을 고려가능
  type obj = {a: 'world'} & {b: 'hello'} 
  
  //요런식으로 가능
  type Animal = {breath: true};
  type Mammalia = Animal & {breed: true};
  type Human = Mammalia & {think: true};
  
  const a: Human = {breath: true, breed: true, think: true};
  // 아래는 talk: true 없어서 에러
  // const a: Human = {breath: true, breed: true, think: true, talk: true}; 

Interface

말그대로 인터페이스 java에서 사용하는 그것과 같음, 상속이 쉬움

  interface A{
  	breath: true,
  }
  
  interface B extends A{
  	breed: true,
  }

또한 중복해서 사용하면 추가하는 개념이 된다. 타입은 중복선언 안됨

  interface A{
  	breath: true,
  }
  
  interface A{
  	breed: true,
  }

void 의 개념

void가 리턴타입으로 명시 되어있을시 매개변수나 매서드안에서 돌려주는 void타입은 사실상 어떤 것이 와도 pass한다. 단 함수 자체의 리턴 타입이 void이면 반드시 void여야한다.

function forEach(arr: number[], callback: (el: number) => void): void;
 
 let target: number[] = [];
forEach([1,2,3], el => target.push(el)) // target.push(el) 는 void 타입이 아니다. 하지만 실행(pass)이 됨
 
 interface Human { 
 	talk: () => void;
 } // 휴먼의 talk가 void 매서드타입이라면...
 
 const human: Human = {
 	talk() {return 'abc';}
 } // 다음과 같을때 string 이어도 상관없다.
 
 function a(): void {
 	return null; 
   //void 타입의 함수에 return을 다른것을하면? 에러
 }

이는 위의 예제와 같이 push와 같이 리턴타입에 상관없이 작업이 실행되어야하는 js의 자유도를 타입스크립트가 전부 막지는 못하기 때문에 허용한것.

TypeScript 활용

본격적으로 타입을 가드하여 실제 사용되는 코드에 해당 타입이 아니면 실행이 안되거나 컴파일단계에서 에러가나타나게 해줄 수 있다.

TypeScript의 unknown 과 any type

  interfact a {
  	talk: ()=> void;
  }
  //any의 경우 다허용이다.. 그런데 이럴거면 TypeScript 왜씀?
  const b: any = a.method();  
  // 그래서 unknown을 사용하자. 이 경우 interface A에는 method라는 매서드가 없다. 그래서 경고를 띄워줌
  const b: unknown = a.method();  

타입가드, 타입 좁히기

타입을 여러가지 허용하는 변수에서 타입에 맞게 찾아가도록 만드는 기법

  fuction numOrStr(a: number | string) {
   if(typeof a === 'number')
  	a.toFixed(1);
  }else { // 알아서 string인거 추론해준다.
   a.charAt(3);
  }

추가로 in연산자를 활용하는 법

type B = { type: 'b', bbb: string};
type C = { type: 'c', ccc: string};

  fuction typeCheck(a: B | C){
  	// a.type== 'b'  이런식말고도 다음과 같이 사용가능하다.
  	if('bbb' in a){
  		a.type;
  	}
  }

  const human = { talk(); }
  const dog = { bow(); }

  if('talk' in a)

타입가드 : 타입이 커스텀일때

예측되는 Pet의 값을 Fish라고 지정한다.
그리고 로직을 이용해서 상세한 타입을 좁혀나갈 수 있다.

function isFish(pet: Fish | Bird): pet is Fish {
  return (pet as Fish).swim !== undefined;
}
  
  let pet = getPet();
  
  if(isFish(pet)){
  	pet.swim();
  } else {
  	pet.fly();
  }

readonly

읽기 기능으로만 타입선언하여 변수를 사용가능하다.

interface A{
 	readonly a: string;
 	b: string;
 }
 // a의 객체의 타입들을 전부 string으로 하고싶다.
 type a =  {[key in string]: number}
 const test:a = { a: 12, b: 34, c: 432};
 // a의 타입을 mapping가능하다 (mapped type)
  type b = 'A'|'B'|'C'
   type a =  {[key in b]: b} // 뒤의 타입도 설정가능
 const test:a = { A: 'C', B: 'A', C: 'B'};

제너릭 활용하기

타입이 섞여있고 return값을 일관되게 설정하고 싶을때는 제너릭을 이용한다.

  //다음과 같은 코드가있다.
  function add(x: string | number, y: string | number) {
   return x + y;
  }
  // 내가 원하는건 다음과 같다.
   add(1, 2);
   add('1', '2');
  // 이때 or 유니온 조건이므로 아래의 펑션도 실행이 된다. 하지만 원치않는다면?
   add(1, '2');
  add('1', 2);
  // 해답은, 제너릭을 사용한다.
  function add<T> (x: T, y: T): T {
   return x + y;
  }
  // T는 임의의 단어이며 K, X, ABC 어떤게 와도 상관없다. 또한 상세하게 설정이 가능하다.
    function add<T extends number, K extends number> (x: T, y: K): T {
   return x + y;
  }
  
  // 다음의 기능도 가능하다.
  <T extends {...}> // {a: string}
  <T extends any[]> // string[]
  <T extends (...arg: any) => any> // (a: string) => number
  <T extends abstract new (...args: any) => any> // abstract new (...arg: any) => any

직접 함수타입 짜보기 feat: forEach, map, filter

  1. interface 를 만든다.
    interface Arr {
    	// forEach라는 매서드가 있고, callBack이라는 매개변수가 있는데, 이는 함수이고 () => return
    	// 이때 이 함수에 변수 item 은 string return 은 void타입이다.  forEach의 전체 리턴값도 void이다.
    	forEach(callBack: (item: string) => void): void;
    }
  1. 이때 타입에 따른 예제를 만들어 테스트를 진행해보자.
 	const a: Arr = [1,2,3];
    a.forEach((item)=> {
    	console.log(item);
    	item.toFixed(1); // 에러발생
    })
    
    const b: Arr = [1,2,3];
    b.forEach((item)=> {
    	console.log(item);
    	item.charAt(3); // 타입이 맞으므로 괜찮음
    })
  1. 그럼 타입을 변경을 진행한다. 제너릭을 사용하여 타입을 확장해준다.
   interface Arr<T> { // T를 사용하여 Arr의 타입을 정해준다.
    	forEach(callBack: (item: T) => void): void; // 마찬가지로 입력된 T와 같게 지정한다.
    }

    const a: Arr<number> = [1,2,3]; //이젠 타입을 지정해줘야한다.
  1. Map 만들어보기
   interface Arr<T> {
    	forEach(callBack: (item: T) => void): void; 
        // T를 이용하는것은 forEach에서 사용했으니 다음과 같이 선언한다. map이므로 리턴은 배열타입으로...
    	map(callback: (item: T) => T): T[]; 
    }
  1. 예제 테스트하기
   const a: Arr<number> = [1, 2, 3];
    
    const b = a.map((item) => item + 1);
    // 이부분에서 에러. item이 string이 아니므로 toString() 에서 에러가남
    const c = a.map((item) => item.toString()); 
  1. map에러난거 해결하기
    interface Arr<T> {
    	forEach(callBack: (item: T) => void): void; 
        // 위에서 결과를 보듯 들어온 값(T)과 나간 값(S)의 타입이 다르므로 다르게 설정해야한다.
    	map<S>(callback: (item: T) => S): S[]; 
    }
  1. filter만들기
    interface Arr<T> {
    	forEach(callBack: (item: T) => void): void; 
    	map<S>(callback: (item: T) => S): S[];
    	filter(callback: (v: T) =>v):T[];
    }

    
    const a:Arr<number | string> = [1,'2',3,'4',5];
    //['2', '4'] 하지만 return type을 확인하면 number | string이다, string이 되어야함.
    const d: c.filter((v)=> typeof v === 'string');
  1. return 제너릭 S
    //위에서  map과 마찬가지로 변수의 타입이 결과의 타입하고 다르므로 S로 선언해야함.
    interface Arr<T> {
       forEach(callBack: (item: T) => void): void; 
       map<S>(callback: (item: T) => S): S[];
       // filter는 return시 값은 그대로 돌려주므로 v를 리턴 그리고 해당 타입을 S로 변경
       //하지만 T가 S로바뀔 수는 없음 따라서 앞에 extends로 확장시킴
       filter<S extends T>(callback: (v: T) =>v is S):S[]; 
    }

TypeScript 활용2

유틸리티 타입 분석하기

타입스크립트에는 타입을 커스텀 할 수 있는 다양한 유틸이 많이 있다.

Partial Type

특정 인터페이스나 클래스 타입을 선언시 옵셔널로 부분적으로 가져오게 할 수 있는 타입 (있어도, 없어도 상관없는 타입)

interface Profile {
	name: string,
    age: number,
    married: boolean,
}
const a: Partial<Profile> = {
	name: 'shin',
    age: 44,
    // 부분적으로 가저올 수 있으므로 married 생략가능
}
    
// 구현체
type P<T> = {
    [Key in keyof T]?: T[Key]
}
    
const b: P<Profile> = {
	married: true;    
}

Pick Type

필수로 선언해야할때 작성

  	interface Profile {
      	name: string,
      	age: number,
      	married: boolean,
  	}
    
    const a: Pick<Profile, 'name' | 'age'> ={
    	name: 'shin',
    	age: '44',
    }
    
    // 구현체
    type P<T, S extends keyof T> = {
        [Key in S]: T[Key];
    }

    const b: P<Profile, 'married'> = {
        married: true;
    }

Exclude type

//S가 T의 부모가 아니어야 통과한다.
type Ec<T,S> = T extends S ? never : T;

Extract type

//S가 T의 부모야만 통과한다.
type Et<T,S> = T extends S ? T : never;

Omit Type

'제외'하고 필수로 선언해야할때 작성

  	interface Profile {
      	name: string,
      	age: number,
      	married: boolean,
  	}
    
    const a: Omit<Profile, 'name'> ={
    	age: '44',
    	married: ture,
    }
    
    // 구현체는 Pick과 Exclude 구현체를 사용한다.
    // S extends keyof any 뒤에는 key값이 와야하므로 다음과 같이 선언
    type O<T, S extends keyof any> = Pick<T, Exclude<keyof T, S>>

    const b: O<Profile, 'married'> = {
        married: true;
    }

여기까지 존재하는 유틸을 직접 만들어봤고, 그 밖에도 Required, Record, NonNullable많은 유틸들이 존재한다.




참고자료들___
(참고)인프런 타입스크립트 올인원 강의

profile
하이

0개의 댓글