ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Typescript] 기본적인 타입스크립트 문법과 키워드 정리
    언어, 프레임워크/Javascript & Typescript 2021. 10. 6. 17:10

     

    인프런 강의를 듣고 배운 내용에 대해 정리한 글입니다.

    틀린 부분이 있다면 말씀 부탁드립니다!


     

    올해가 가기 전 타입스크립트를 공부해야겠다고 생각했고 그 첫 단계로 타입스크립트 입문 강의를 들었다. 여러 언어 중 타입 스크립트를 선택한 이유는 1) 트랜디한 언어라는 점 2) 자바스크립트를 사용하고 있는 입장에서 러닝 커브가 적을 것 같다는 점 3) 자바스크립트와 뭐가 다른지 직접 써보고 확인하고 싶다는 점 때문이었다.

    출처: https://octoverse.github.com/#overview

    강의를 수강한 뒤 든 생각은 '자바스크립트의 확장판 느낌이군 + (자바스크립트에 비해) 사용하기 좀 번거롭네' 였다. 자바스크립트에선 신경 안 써도 됐던 타입 정의라던가, 그에 맞는 문법이라던가... 굳이 왜 이렇게까지 작성해야 되지?라는 생각이 들었다.

     

    굳이 귀찮게 일일이 타입을 정하고 그에 맞게 코드를 짜는 이유는 아래와 같다.

     

    자바스크립트는 타입 정의없이 유연하게 사용할 수 있어서 편하지만 이 특징이 단점이 되기도 한다. 예를 들어, 배열인데 잘못된 타입의 함수를 썼다가 런타임 에러가 발생한다던가, vscode 자동완성 기능을 충분히 활용하지 못한다던가 등등...(생각해보니 나도 작업하면서 다른 분이 작성한 코드에서 특정 값의 형태를 알아내기 위해 변수 선언부까지 스크롤을 올렸던 적이 종종 있었다) 명확한 타입을 정하면 일종의 약속이 생기는 것이므로 다수가 작업하는 환경에서 해당 타입에 맞게 코드를 작성할 것이고, 설사 에러가 발생해도 개발 도중에 알아차릴 수 있으므로 소름 끼치는 일이 발생할 확률은 현저히 감소한다. 또한 미리 타입을 정해놓기 때문에, vscode가 타입을 추측하여 알맞은 속성과 함수를 리스트업 하여 개발 속도를 높여준다. 타입스크립트를 사용해야 하는 이유를 정리하면,

     

    • 잘못된 타입 사용으로 인한 런타임 에러가 발생할 확률이 줄어든다.
    • 장기적으로 봤을 때 개발 효율성이 증가한다.

     

    타입스크립트의 필요성을 알았으니, 타입스크립트의 기본 문법 및 자바스크립트만 사용했을 때는 몰랐던 키워드를 정리해보겠다.

     

    1. 기본 타입 종류

    숫자, 문자열, 배열, 튜플, 객체, void ... 이 외에 몇 개 더 있음

    https://medium.com/humanscape-tech/typescript-%EA%B8%B0%EB%B3%B8-%ED%83%80%EC%9E%85-9e35b43408fe

     

    TypeScript 기본 타입

    안녕하세요. Humanscape의 개발자 David입니다.

    medium.com

     

    2. 변수에 타입 선언

    // 1. 문자열 타입 선언
    let a:string;
    let b:string = 'hello';
    a = 'world';
    a = 1000; // error
    
    // 2. 숫자 타입 선언
    let c: number = 20000;
    
    // 3. 배열 타입 선언
    let d: Array<string> = ['h', 'e', 'l', 'l', 'o']; // 문자열을 속성으로 갖는 배열
    let e: Array<number> // 숫자를 속성으로 갖는 배열
    let f: Array<object> // 객체를 속성으로 갖는 배열
    // cf) Array<string>을 <string>[] 로 표현할 수 있다.
    
    // 4. 객체 타입 선언
    let g: object = {};
    let h: { name: stirng, age: number } = { name: 'gildong', age: 20 };
    
    // 5. boolean 타입 선언
    let i: boolean = true;
    let j: boolean = 'false'; // error
    
    // 6. 튜플 타입 선언
    let k: [number, number, string] = ['hello', 'world', 1000];
    
    // 7. undefined, null 타입 선언
    let l: undefined;
    let m: null;

     

    3.  함수에 타입 선언

    // 1. 함수의 파라미터 및 리턴값의 타입 선언
    const sum = (param1: number, param2: number):string => { 
    	let sumResult = param1 + param2;
    	return '합계: ' + sumResult;
    };
    sum(10, 100);
    sum(10, '100'); // error
    
    // 2. 함수의 파라미터 개수가 유동적으로 변경될 경우 파라미터 타입 선언 (옵셔널 파라미터)
    // 함수에 커서 올리면 아래처럼 나타남 
    // const getNameList: (param1: string, param2?: string | undefined) => Array<string>
    const getNameList = (param1: string, param2?: string):Array<string> => {
    	return param2 ? [param1, param2] : [param1];
    };
    getNameList('gildong', 'dingdong'); // ['gildong', 'dingdong']
    getNameList('gildong'); // ['gildong']
    
    // 3. 리턴값이 없는 함수의 반환값 타입 선언
    const returnNothing = ():void => {
    	console.log('nothing');
    };
    returnNothing();

     

    4. 인터페이스

    // 인터페이스 선언
    interface User {
        name: string;
        age: number;
    };
    
    // 1. 인터페이스 활용하여 변수에 타입 선언
    let gildong: User = { name: '길동', age: 20 };
    
    // 2. 인터페이스 활용하여 함수 파라미터 타입 선언
    const getUserInfo = (param: User):string => {
    	return User.name + '은 ' +  User.age + '세 입니다.';
    };
    
    // 3. 인터페이스 활용하여 함수 스펙 선언
    interface SumFunc {
      (param1: number, param2: number): number;
    };
    const sum: SumFunc = (n1, n2) => {
        return n1 + n2;
    };
    sum(10, 20)

     

    5. 오퍼레이터 (Union type & Intersection type)

    Union type 

    // 파라미터는 string 또는 number 타입을 사용할 수 있다.
    const printStrOrNum = (param: string | number): string => {
    	return 'result : ' + param;
    };
    printStrOrNum('hello') // 'result : hello'
    printStrOrNum(20000) // 'result : 20000'
    
    // 유니온 타입의 경우, 교집합에 해당하는 함수 및 속성만 사용 가능.
    const printStrOrNum = (param: string | number) => {
    	param.toUpperCase() // error
    };
    // 에러내용
    // Property 'toUpperCase' does not exist on type 'string | number'.
    // Property 'toUpperCase' does not exist on type 'number'
    
    // 유니온 타입을 사용할 때 각 타입에 맞는 함수를 사용하기 위해선 조건절 사용
    const printStrOrNum = (param: string | number) => {
      if (typeof param === 'string') {
        param.toUpperCase();
      } else {
        param;
      };
    }

     

     Intersection type

    interface Coffee {
    	shot: number
    }
    
    interface Drink {
    	name: string
    }
    
    // 파라미터는 Coffee와 Drink 타입 모두 충족해야한다.
    const makeLatte = (param: Coffee & Drink) => {
        param.shot = 2;
        param.name = 'mlik'
        
        return param;
    };
    makeLatte({shot: 0, name: ''}) // { "shot": 2, "name": "mlik" } 
    
    // string과 number 모두되는 값은 없으므로 never
    const stringAndNumber = (param: string & number) => {
        return param;
    };
    stringAndNumber('hello') // error
    // 에러내용
    // Argument of type 'string' is not assignable to parameter of type 'never'.

    cf) never type은 any type의 반대로 어떤 것도 할당될 수 없는 타입.

     https://www.explainprogramming.com/typescript/never-type/

     

    Typescript `never` type

    Discussions about the `never` type in Typescript

    www.explainprogramming.com

     

    6. 클래스

    class newPerson{
      // JS ES6와 달리 class몸체에 클래스 프로퍼티 사전 정의 필요
      name: string;
      age: number;
    
      constructor(name: string, age: number){
        this.name = name,
        this.age = age
      }
    }

     

    7. 제네릭

    // 제네릭을 사용하여 함수 정의
    const funcGeneric = <T>(param: T): T => {
      return param;
    };
    // 타입을 파라미터처럼 넘겨주기 때문에, 유연하게 활용 가능 = 재사용에 유리
    funcGeneric<string>('hello');
    funcGeneric<number>(10000);
    
    
    // 제네릭을 사용하여 인터페이스 정의
    interface User<T> {
    	name: T,
        age: number
    }
    const gildong<string>User = { name: 'gildong', age: 20 }
    
    
    // 제네릭을 사용하면 타입이 정의되지 않아 타입 추측이 안됨
    const funcGeneric2 = <T>(param:T): T => {
      return param.length; // error: param의 타입이 정의되기 전이므로 에러 발생
    }
    
    // 제네릭 타입제한 방법 1
    interface existLenth {
    	length: number
    }
    // 타입 T는 existLenth 타입의 하위 타입이다
    const funcGeneric2 = <T extends existLenth>(param:T) => {
      return param.length;
    }
    funcGeneric2(1000); // error
    funcGeneric2('hello');
    funcGeneric2({ length: 10000 });
    funcGeneric2(['h', 'e', 'l', 'l', 'o']);
    
    // 제네릭 타입제한 방법2
    interface Fruit {
    	apple: number
    }
    // T타입은 Fruit타입 속성중의 하나
    const getFruit = <T extends keyof Fruit>(param: T): number => {
        return param;
    }
    
    getFruit('apple');
    getFruit('melon'); // error
    
    // 2개 이상의 제네릭 타입 적용
    interface Fruit <T> {
      name: T,
    }
    
    interface Apple<K> extends Fruit<K> {
      color: K,
      weight: number
    }
    
    const item: Apple<string> = {
      name: 'apple',
      color: 'red',
      weight: 500
    }

     

    8. 타입 가드

    interface Developer {
      name: string,
      skill: string
    }
    interface Person {
      name: string,
      age: number
    }
    const getDeveloperInfo = (): Person | Developer  => {
      return { name: 'gildong', skill: 'ts', age: 20}
    }
    // union 타입을 사용했으므로 Person타입과 Developer타입의 교집합인 속성만 사용 가능 
    const dev1 = getDeveloperInfo(); 
    
    // 타입 단언 사용 (키워드: as) --> 중복 코드가 발생
    if ((dev1 as Person).age) {
      console.log((dev1 as Person).age)
    } else if ((dev1 as Developer)) {
      console.log((dev1 as Developer).skill)
    }
    
    // 타입 가드 사용 (키워드: is)
    // 1. target은 Person | Developer 타입이다.
    // 2. (target as Developer).skill !== undefined이면 target은 Developer 타입이다.
    const isDeveloper = (target: Person | Developer): target is Developer => {
      return (target as Developer).skill !== undefined;
    }
    if (isDeveloper(dev1)) {
      console.log(dev1.skill)
    } else {
      console.log(dev1.age)
    };

     

    9. 타입 호환

    interface Person {
      name: string,
      age: number
    }
    interface Student {
      name: string
    }
    class setPerson {
      name: string;
      age: number;
      addr: string;
    }
    let person: Person
    let student: Student
    
    // 1. 인터페이스 & 클래스
    student = person // Student가 Person보다 타입 구조가 작으므로 호환 가능
    student = new setPerson() // Student가 setPerson함수보다 타입 구조가 작으므로 호환 가능
    person = student // error. 타입 구조가 큰 대상에 작은 타입 구조 대상은 호환 불가
    
    // 2. 함수
    let add = (num1: number) => {
      // ... 
    }
    let sum = (num1: number, num2: number) => {
      // ...
    }
    sum = add; // ok
    add = sum; // error. 함수의 경우, 구조적으로 작은 함수에 큰 함수를 호환할 수 없다 (sum은 파라미터 2개, add는 파라미터 1개)
    
    // 3. generic
    interface getTag<T> {
      tag: T
    }
    let tag1: getTag<string>;
    let tag2: getTag<number>;
    // tag1 = tag2; // error.
    // tag2 = tag1; // error. 제네릭을 사용하여 인터페이스의 타입 구조가 달라지므로 타입 호환이 불가능
    
    interface getNoTag<T> {
      // ...
    }
    let tag3: getNoTag<string>;
    let tag4: getNoTag<number>;
    tag3 = tag4; // ok
    tag4 = tag3; // ok. 제네릭을 사용했지만 인터페이스 타입 구조가 제네릭에 의해 변경되지 않았으므로 타입 호환 가능

     

    댓글

jaejade's blog ٩( ᐛ )و