TypeScript
20210506 TypeSciprt 03 타입시스템, TS의 타입 특성에 따른 옵션들, 나만의 타입 만드는법, 타입호완성(sub 타입, super타입), Type Aliase
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에 담겨져 있음