20210506 TypeSciprt 03 타입시스템, TS의 타입 특성에 따른 옵션들, 나만의 타입 만드는법, 타입호완성(sub, super), Type Aliase

7 분 소요

TypeScript03

타입 시스템

  • 컴파일러에게 사용하는 타입을 명시적으로 지정하는 시스템
  • 컴파일러가 자동으로 타입을 추론하는 시스템


타입스크립트의 타입 시스템

  • 타입을 명시적으로 지정가능하고 지정하지 않으면, 타입스크립트 컴파일러가 자동으로 타입을 추론


형태를 정해둔 함수의 사용자와 구현자

  • 타입이란 해당 변수가 할수 있는 일을 결정함
    • 함수의 인자가 할 수 있는 일은 함수 인자의 타입이 결정함
const f1 = (a) => {
  return a;
};


함수 사용법에 대한 오해를 야기하는 JS

  • 사용법을 알기 위해서는 구현자에게 물어보거나 함수가 어떻게 이루어져 있는지 뜯어봐야 앎
const f2 = (a) => {
  return a * 38;
};
console.log(f2(10)); // 380
console.log(f2("Tom")); // NaN -> 구현자가 원하지 않는 결과 대로 사용자가 사용할 가능성이 있음


TS의 추론에 의지하는 경우

  • 함수의 인자는 명확하게 type이 명시 되어 있지 않기 때문에 TS가 추론하여 type을 지정하게 되는데 any로 지정됨 (무엇이든지 들어갈 수 있음)
  • return type의 경우 number로 추론되어 지지만 NaN도 number 이기 때문에 NaN을 의도하지 않으면 잘못 사용하게 될 수 있음
  • 사용자는 TS에 의한 type 사용방법을 알고 있음에도 잘못 사용하게 됨
const f3 = (a) => {
  return a * 38;
};
console.log(f3(10)); // 380
consoel.log(f3("Tom") + 5); //NaN
  • 이에 대한 해결 방법으로 noImplicitAny 옵션을 사용하게 되면 명시적으로 지정하지 않은 경우에 TS가 추론하여 any로 지정되게 되면 컴파일 에러를 발생시켜 type을 명시적으로 적어주게 유도 하여 실수를 줄일 수 있음


noImplicitAny 에 의한 방어

  • 옵션을 키게 되면 error TS7006: Parameter 'a' implicitly has an 'any' type. 의 error를 뿜게 됨
  • 작성자가 작성한 함수에 에러가 발생함에 따라 사용자는 해당 함수를 사용할 수 없음 (맞게 사용한다고 해도 이미 작성시 error가 있기 때문에)
  • 그래서 컴파일이 정상적으로 실행 될 수 있도록 코드 수정을 해줘야 함


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

  • 함수의 매개변수에 number type을 명시적으로 해주어도 return type 의 경우 명시하지 않은 경우 return type은 TS에 의해서 추론하여 type이 지정되게 됨
const f4 = (a: number) => {
  if (a > 0) {
    return a * 38;
  }
};
console.log(f4(-5) + 5); // NaN
  • 위처럼 함수를 작성하게 되면 return은 number로 추론되게 됨
  • 하지만 함수에서 body 부분은 컴파일로 잡아내지 못하고 runtime에 의해서 결과를 확인 할 수 있기 떄문에
  • 사용자가 a에 음수를 넣는 경우 if 에 return이 없어 undefined가 나타나게 됨
  • 결국에는 에러를 못잡고 결과값이 undefined 임에도 불구하고 number로 인식하게 됨
  • 즉, number에 undefined 또는 null을 넣을 수 있게 되는 문제가 생김


strictNullChecks 옵션

  • strictNullChecks 옵션을 사용하게 되면 모든 타입에 자동으로 포함되어 있는 null, undefined를 제거해 주게 됨
  • 그러므로 켜야함!
const f4 = (a: number) => {
  if (a > 0) {
    return a * 38;
  }
};
console.log(f4(-5) + 5); // error TS2532: object is possibly 'undefined'
  • 위처럼 옵션을 킨 상태에서는 함수의 리턴 타입이 유니온으로 number | undefined 로 추론됨
  • 그래서 undefined + 5 로 인식하여 에러를 받을 수 있게 된다.
  • 하지만 정상적인 값의 경우의 type도 유니온으로 지정되는데 런타임 상 인자에 어떤 값이 들어 올지 모르기 때문에 컴파일 타임에 판단할 수가 없음
  • 그렇기 때문에 undefined가 나와도 나중에 런타임 상에서 에러로 연산을 막을수 있게 해야 함


명시적으로 return type을 지정하는 경우

const f5 = (a: number): number => {
  if (a > 0) {
    return a * 38;
  }
};
  • 위처럼 return type도 명시적으로 type을 지정하는 경우 함수 구현부의 body 부분의 return type과 명시적으로 return을 지정한 type이 일치하지 않기 때문에 컴파일 에러가 발생함 (number vs number undefined 로 인한 충돌 -> else 부분에 대한 return 작업이 없기 때문에)


noImplicitReturns 옵션

  • noImplicitReturns 옵션을 사용하게 되면 함수 내에서 모든 코드가 값을 리턴하지 않으면, 컴파일 에러를 발생 시킴
  • 함수 구현부 헤드에 ruturn 값이 명시 되어 있든 아니든 함수 body에 있는 코드들이 나누어 지는 경우의 경로마다 return을 명시적으로 해주게 강제할 수 있음


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

  • 매개변수에 object가 들어와서 활용해야 하는 경우인데 명시적으로 타입이 지정이 안되 있는 경우
  • 어떤 값이든 들어오게 되어 사용자가 원치 않는 함수 결과값을 만들어 낼수 있음
const f6 = (a) => {
  return `이름은 ${a.name} 이고, 연령대는  ${
    Math.floor(a.age / 10) * 10
  }대 입니다.`;
};
console.log(f6({ name: "mark", age: 24 })); // 정상 출력
console.log(f6("mark")); // 이름 undefined 이고, 연령대는 NaN대 입니다 라는 잘못된 출력 결과 발생


object literal type

  • 매개변수에 object 가 들어 오는 경우 object literal type으로 명시 해야함
const f7 = (a: { name: string, age: number }): string => {
  return `이름은 ${a.name} 이고, 연령대는  ${
    Math.floor(a.age / 10) * 10
  }대 입니다.`;
};
console.log(f6("mark")); // TS가 컴파일 에러 발생해서 확인 가능
  • 하지만 매번 길게 object literal type으로 쓰기는 힘들기 때문에 자신만의 type을 만들어 쉽게 쓸수 있게 해야함




나만의 타입을 만드는 방법

interface를 이용한 방법

interface PersonInterface {
  name: string;
  age: number;
}


TypeAlias를 이용한 방법

type PersonTypeAlias = {
  name: string,
  age: number,
};
  • 적용법
const f8 = (a: PersonInterface): string => {
  return `이름은 ${a.name} 이고, 연령대는  ${
    Math.floor(a.age / 10) * 10
  }대 입니다.`;
};




Structural vs Nominal type system

structural type system

  • 구조가 같으면, 같은 타입임
  • Typescript 에서 사용하는 system 임
interface IPerson {
  name: string;
  age: number;
  spaek(): string;
}

type PersonType = {
  name: string;
  age: number;
  spaek(): string;
}

let personInterface: IPerson = {} as any;
let personType: PersonType = {} as any;

personType = personInterface;
personInterface = personType;


nominal type system

  • 구조가 같아도 이름이 다르면, 다른 타입임
  • C, Java 에서 사용됨 (TS 따르지 X)

  • 의도적으로 typescript에서 구조가 같아도 다르게 처리해야 하는 경우가 생길 수도 있음
  • 극단적인 사용 예시
type PersonID = string & { readonly brand: unique symbol};

function PersonID(id: string): PersonID {
  return id as PersonID;
}
function getPersonById(id:PersonID) {}

getPersonById(PersonID('id-aaaaa'));
getPersonById('id-aaaaa'); // error


Duck typing

  • runtime에 발생하는 typing 방식
  • 만약 어떤새가 오리처럼 걷고, 헤엄치고, 꽥꽥거리는 소리를 낸다면 나는 그 새를 오리라고 부를 것이다.
  • Python에서 사용됨
class Duck:
  def sound(self):
    print u"꽥꽥"

class Dog:
  def sound(self):
    print u"멍멍"

def get_sound(animal):
  animal.sound()

def main():
  bird = Duck()
  dog = Dog()
  get_sound(bird)
  get_sound(dog)




타입 호완성 (Type Compatibility)

서브 타입

  • sub type은 super type에 할당 가능하고 반대로는 불가하다.
  • 보통 literal은 sub이고 primitive는 super 느낌이다.
let sub1: 1 = 1;
let sup1: number = sub1;
sub1 = sup1; // error


  • 그리고 배열 같은 경우, object에 포함되기 때문에 넣을 수 있다. (반대는 안됨)
let sub2: number[] = [1]; // 배열
let sup2: object = sub2;
sub2 = sup2; // error


  • 그리고 구성하는 type이 같은 튜플의 경우는 배열에 포함 될 수 있다.
let sub3: [number, number] = [1, 2];
let sup3: number[] = sub3;
sub3 = sup3; // error


  • any와 다른 type과의 관계는 어떤 것이든 서로 포함 가능하다. (예외)
let sub4: number = 1;
let sup4: any = sub4;
sub4 = sup4; // 가능함 (주의)


  • never와 다른 type과의 관계에서 never는 어떤 것이든 sub로 들어갈 수 있음
let sub5: never = 0 as never;
let sup5: number = sub5;
sub5 = sup5; // error


  • 클래스 상속 관계의 경우
class Animal {}
class Dog extends Animal {
  eat() {}
}

let sub6: Dog = new Dog();
let sup6: Animal = sub6;
sub6 = sup6; // error


  • 공변 : 같거나 서브 타입인 경우, 할당이 가능함
// primitive type (범위의 포함 관계)
let sub7: string = "";
let sup7: string | number = sub7;

// object - 각각의 프로퍼티가 대응하는 프로퍼티와 같거나 서브타입인 경우 (범위의 포함 관계)
let sub8: { a: string, b: number } = { a: "", b: 1 };
let sup8: { a: string | number, b: number } = sub8;

// array - object
let sub9: Array<{ a: string, b: number }> = [{ a: "", b: 1 }];
let sup9: Array<{ a: string | number, b: number }> = sub8;


  • 반병 : 함수가 할당 될때 할당되는 함수의 매개변수 타입이 같거나 슈퍼타입인 경우, 할당이 가능하다.
// 가지는 범위를 생각해 보면 됨 상속 되면 될수록 점점 overide 되면서 범위가 커지기 때문에
class Person {}
class Developer extends Person {
  coding() {}
}
class StartupDeveloper extends Developer {
  burning() {}
}

function tellme(f: (d: Developer) => Developer) {}

// 함수를 인자로 받는데 이 함수가 Developer를 인자로 받아 Developer를 return 해야함

tellme(function dToD(d: Developer): Developer {
  return new Developer();
}); // 문제 없음!

tellme(function pToD(d: Person): Developer {
  return new Developer();
}); // 문제 없음!

tellme(function sToD(d: StartupDeveloper): Developer {
  return new Developer();
}); // burning을 가능하게 하기 때문에 논리적으로 문제가 있음 -> 하지만, 옵션을 통해 사용자에게 융통성 있게 사용가능하게 할 수도 있음
  • strictFunctionTypes 옵션을 켜면 함수를 할당하는 경우 할당하는 함수의 매개변수 타입이 같거나 슈퍼타입인 경우는 허락하고, 그외에(subtype)는 에러를 통해 경고한다.




타입 별칭 (Type Alias)

  • interface랑 비슷해 보임
  • Primitive, Union Type, Tuple, Function 등을 기타 직접 작성해야 하는 타입을 다른 이름으로 지정 가능
  • 만들어진 타입의 refer로 사용하는 것이지 타입을 만드는 것은 아님
  • 반복을 줄이고 타이핑을 줄이기 위함

Aliasing Primitive

type MyStringType = string;
const str = "world";
let myStr: MyStringType = "hello";
myStr = str; // 가능

Aliasing Union Type

let person: string | number = 0;
person = "Mark";

type StringOrNumber = string | number;

let another: StringOrNumber = 0;
another = "Anna";

Aliasing Tuple

type PersonTuple = [string, number];

let another: PersonTuple = ["Anna", 25];

Aliasing Function

type EatType = (food: string) => void;
  • aliase 와 interface의 구분
    • type으로서 목적 존재가치가 명확하다면 interface로 사용하고
    • 단지, 대상을 가르키고 별명으로 사용된다면 aliase를 사용함
    • 본인만의 기준을 세워서 사용하도록 하자




Compilation context

  • 어떠한 방식으로 해당 파일을 그룹핑 하여 compile 할것인지, 어떠한 옵션으로 compile 할것인지에 대한 맥락을 Compilation context라고 하고 해당 사항은 tsconfig.json에 담겨져 있음