TypeScript: Type System Ep.1 작성자와 사용자의 관점으로 코드 바라보기

작성자와 사용자의 관점으로 코드 바라보기


타입시스템

  • 두가지 형태
    • 컴파일러에게 사용하는 타입을 명시적으로 지정하는 시스템
    • 컴파일러가 자동으로 타입을 추론하는 시스템
  • 타입스크립트의 타입 시스템
    • 타입을 명시적으로 지정할 수 있다.
    • 타입을 명시적으로 지정하지 않으면, 타입스크립트 컴파일러가 자동으로 타입을 추론한다. 타입스크립트의 타입 추론이라고 부른다.
  • 함수 사용법에 대한 오해를 야기하는 자바스크립트

    타입은 해당 변수가 할 수 있는 일을 결정한다. 따라서 a가 할 수 있는 일은 a의 타입이 결정한다.

    해당 함수의 작성자는 매개변수 a가 number 타입이라는 가정으로 함수를 작성했다. 하지만 사용자는 사용법을 숙지하지 않은 채, 문자열을 사용하여 함수를 실행했다. 그래서 의도하지 않은 NaN 결과가 나오게 되었다.

    function f2(a) {
      return a * 38;
    }
      
    console.log(f2(10)); // 380
    console.log(f2('Mark')); // NaN
    
  • 타입스크립트의 추론에 의지하는 경우

    타입스크립트는 타입을 지정하지 않으면 추론을 한다. a의 타입을 명시적으로 지정하지 않았기에 a는 any로 추론된다. 그래서 a 인자는 어떤 형태이던지 다 사용이 되는 형태로 볼 수 있다.

    function f3(a) {
      return a * 38;
    }
      
    console.log(f3(10)); // 380
    console.log(f3('Mark') + 5); // NaN
    

    사용자 입장에서 a가 any이기 때문에, 사용법에 맞게 문자열을 사용하여 함수를 실행했다. 해당 결과로 NaN(NaN도 number의 하나)이 나왔는데, 만약 해당 결과가 작성자가 의도한 것이 아니라면 해당 함수는 올바르지 않은 함수이다.
    따라서 해당 함수의 작성자는 함수의 올바른 사용법을 사용자에게 전달하지 못한 것이다.
    해당 함수에서 a가 타입을 지정하지 않았기에 any로 추론이 되면서 위험한 요소가 되어버린다. 위험한 요소를 없애기 위해서 컴파일러에게 이러한 형태를 작성자가 사용할 수 없도록 하는 방식이 있다. 해당 방식이 nolmplicitAny.

  • nolmplicitAny

    타입을 명시적으로 지정하지 않은 경우, 타입스크립트가 추론 중 any라고 판단하게 되면, 컴파일 에러를 발생시켜 명시적으로 지정하도록 유도한다.

    • nolmplicitAny에 의한 방어
      // error TS7006: Parameter 'a' implicitly has an 'any' type.
          
      function f3(a) {
        return a * 38;
      }
          
      // 사용자의 코드를 실행할 수 없습니다. 컴파일이 정상적으로 마무리 될 수 있도록 수정해야 합니다.
          
      console.log(f3(10)); 
      console.log(f3('Mark') + 5); 
      

      작성자의 코드 자체가 에러이기 때문에 f3(10)과 같은 것들 조차 사용할 수 없게 된다. 코드에서 a의 타입을 any라고 지정해주거나 다른 타입을 지정해줘야만 사용할 수 있게된다.

  • number 타입으로 추론된 리턴 타입

    매개변수 a의 타입을 number라고 명시적으로 지정했으며, 명시적으로 지정하지 않은 함수의 리턴 타입은 number로 추론된다.

    function f4(a: number) {
      if (a > 0) {
        return a * 38;
      }
    }
      
    console.log(f4(5)); // 190
    console.log(f4(-5) + 5); // NaN
    

    a의 타입을 number로 지정했기 때문에 인수에 숫자가 아닌 다른 타입을 넣게 된다면 error가 발생할 것이다. 그래서 다른 형태를 넣었을 때는 해당 함수를 사용할 수 없다.

    런타임상에서 a의 인자가 숫자형이면서 양수의 값일때 a*38이 되고, 아닐 경우에는 리턴되지 않기 때문에 undefined형태로 리턴될 것을 알 수 있다. console.log(f4(-5) + 5);해당 코드에서는 음수값 -5를 인수로 넣었기 때문에 undefined가 리턴되고 + 5 (숫자)가 더해지면서 숫자가 아닌 의미로 NaN(타입 number)이 출력된다. 해당 결과는 의도한 것이 아니기 때문에 런타임 상에서 오류가 발생하게 된다.

    추가) undefined는 number가 아닌데 왜 둘이 더해서 number(NaN)이 나온 것일까?
    타입스크립트가 undefined를 number로 퉁쳐버렸다.
    해당 결과로 인해 타입스크립트에서 number에 undefined가 포함되어 있다는 사실을 알 수 있다.

  • strictNullChecks

    모든 타입에 자동으로 포함되어 있는 nullundefined를 제거해준다.
    해당 옵션을 키지 않으면 위 함수와 같이 number에도 null, undefined를 넣을 수 있게 된다..!

    // strictNullChecks
      
    function f4(a: number) {
      if (a > 0) {
        return a * 38;
      }
    }
      
    console.log(f4(5)); // 190
    console.log(f4(-5) + 5); 
    // error TS2532: object is possibly 'undefined'.
    

    매개변수의 타입을 number로 지정, 명시적으로 지정하지 않은 함수의 리턴 타입은 number 혹은 undefined로 추론된다.

    console.log(f4(-5) + 5); 해당 코드에서 f4(-5)의 값은 undefined이면서 숫자 5와 더할 수 없게 된다. 옵션을 킴으로 undefined가 number로 퉁칠 수 없게 되었다.

    그런데, f4(5)의 값이 190 혹은 undefined가 되어버렸다. f4를 실행하면 무조건 number 혹은 undefined로 판단하는 것이다. 이것이 문제는 아니다. 컴파일 타입에서는 number | undefined로 판단되는 것이 맞고 런타임 상에서 해당 결과물이 undefined인 경우에는 error를 throw하고 number일때만 연산을 하도록 조정을 해야 한다.

  • 명시적으로 리턴 타입을 지정해야 할까?

    명시적으로 지정해주는 것을 추천한다.
    작성자 입장에서 코드를 작성할 때 number를 인자로 받고 리턴할 걸 계획을 하고 코드를 작성하면 지정해둔 리턴 타입과 실제 결과가 일치하는지를 타입스크립트가 확인해주기 때문에 검토하면서 작업할 수 있어서 좋다.

    // error
    function f5(a: number): number {
      if (a > 0) {
        return a * 38;
      }
    }
    

    해당 코드는 error가 발생한다. if 범위 내에 있지 않은 부분은 리턴되지 않기 때문에 작업이 덜 되었다고 알려준다.

  • nolmplicitReturns

    nolmplicitReturns 옵션을 키면 함수 내에서 모든 코드가 값을 리턴하지 않으면, 컴파일 에러를 발생시킨다.

    // error: Not all code paths return a value.
      
    function f5(a: number){
      if (a > 0) {
        return a * 38;
      }
    }
    

    해당 옵션은 리턴 타입을 지정했는지 안 했는지는 중요하지 않다.
    내부 코드가 모든 경우에 리턴을 하는지 안 하는지가 중요하다. 해당 코드는 if인 경우에만 리턴을 하고 if 범위 밖의 경우에는 리턴을 하지 않기 때문에 error가 발생한다.

  • 매개변수에 object가 들어오는 경우

    a는 object로 매개변수에 object가 들어오는 경우에는 어떤 object가 들어오는지 자바스크립트에는 명시되어 있지 않다. 그래서 다양한 object 형태가 들어오면서 의도하지 않은 결과가 발생하기도 한다.

    function f6(a) {
      return `이름은 ${a.name} 이고, 연령대는 
      ${Math.floor(a.age / 10) * 10}대 입니다.`;
    }
      
    console.log(f6({ name: 'Mark', age: 38}));
    // 이름은 Mark 이고, 연령대는 30대 입니다.
    console.log(f6('Mark'));
    // 이름은 undefined 이고, 연령대는 NaN대 입니다.
    
    • object literal type

      object literal type으로 a의 타입을 지정해줄 수 있다.
      작성자가 의도한 바와 다르게 작성하여 사용할 경우에 error를 발생시켜 사용자에게 알려준다.

      function f7(a: { name: string; age: number }): string {
        return `이름은 ${a.name} 이고, 연령대는 
        ${Math.floor(a.age / 10) * 10}대 입니다.`;
      }
          
      console.log(f6({ name: 'Mark', age: 38}));
      // 이름은 Mark 이고, 연령대는 30대 입니다.
      console.log(f6('Mark'));
      // error: Argument of type 'string' is not assignable to parameter of type '{ name: string; age: number }'.
      

      그런데, object litetal type을 매번 길게 작성하기에 불편하니 특정한 타입으로 뽑아서 타이핑을 만들어둔다.

    • 나만의 타입 만들기

      interface, type, class을 사용해서 이름을 지정하여 사용할 수 있다.

      interface PersonInterface {
        name: string;
        age: number;
      }
          
      type PersonTypeAlias = {
        name: string;
        age: number;
      }
          
      function f8(a: PersonInterface): string {
        return `이름은 ${a.name} 이고, 연령대는 
        ${Math.floor(a.age / 10) * 10}대 입니다.`;
      }
          
      console.log(f6({ name: 'Mark', age: 38}));
      // 이름은 Mark 이고, 연령대는 30대 입니다.
      console.log(f6('Mark'));
      // error: Argument of type 'string' is not assignable to parameter of type 'PersonInterface'.
      

Comments