01 타입스크립트 알아보기
1 타입스크립트와 자바스크립트의 관계 이해하기
- 모든 자바스크립트는 타입스크립트지만, 모든 타입스크립트가 자바스크립트는 아니다.
- TS가 타입을 명시하는 추가적인 문법을 가지기 때문
타입 체커
무엇이 문제인지 알겠나요?
let city = 'new york city';
console.log(city.toUppercase());
// VM359:2 Uncaught TypeError: city.toUppercase is not a function
타입스크립트의 타입 체커
let city = 'new york city';
console.log(city.toUppercase());
// ~~~~~~~~~~~ Property 'toUppercase' does not exist on type
// 'string'. Did you mean 'toUpperCase'?
- 타입 시스템의 목표 중 하나는 런타임에 오류를 발생시킬 코드를 미리 찾아내는 것.
- 타입스크립트가 '정적' 타입 시스템이라는 것은 바로 이런 특징 때문.
- 그러나 타입 체커가 모든 오류를 찾아내지는 않는다.
의도와 다르게 동작하는 코드
무엇이 문제인지 알겠나요?
const states = [
{ name: 'Alabama', capital: 'Montgomery' },
{ name: 'Alaska', capital: 'Juneau' },
{ name: 'Arizona', capital: 'Phoenix' },
// ...
];
for (const state of states) {
console.log(state.capitol);
}
// undefined
// undefined
// undefined
- 어떠한 오류도 없이 실행되지만
state.capitol
에서 오타가 났음을 알 수 있다. - 즉, 오타가 났기 때문에
undefined
가 났던 것
타입스크립트의 타입 체커
const states = [
{ name: 'Alabama', capitol: 'Montgomery' },
{ name: 'Alaska', capitol: 'Juneau' },
{ name: 'Arizona', capitol: 'Phoenix' },
// ...
];
for (const state of states) {
console.log(state.capital);
// ~~~~~~~ Property 'capital' does not exist on type
// '{ name: string; capitol: string; }'.
// Did you mean 'capitol'?
}
- 하지만 어느 쪽이 오타인지 타입스크립트는 알지 못한다.
- 이럴 때 명시적으로
states
를 선언하여 의도를 분명히 한다.
의도를 분명히 한 코드
타입스크립트의 타입 체커
interface State {
name: string;
capital: string;
}
const states: State[] = [
{ name: 'Alabama', capitol: 'Montgomery' },
// ~~~~~~~~~~~~~~~~~~~~~
{ name: 'Alaska', capitol: 'Juneau' },
// ~~~~~~~~~~~~~~~~~
{ name: 'Arizona', capitol: 'Phoenix' },
// ~~~~~~~~~~~~~~~~~~ Object literal may only specify known
// properties, but 'capitol' does not exist in type
// 'State'. Did you mean to write 'capital'?
// ...
];
for (const state of states) {
console.log(state.capital);
}
- 어디서 오류가 발생했는지 알 수 있다.
- 제시한 해결책이 올바르다.
요약
- TS는 JS의 상위집합이다.
- TS는 JS 런타임 동작을 모델링하는 타입 시스템을 갖고 있으므로, 런타임 오류를 발생시키는 코드를 찾아내려 한다. 그러나 타입 체커를 통과하면서도 런타임 오류를 발생시키는 코드는 존재할 수 있다. (ts를 맹신하지 말자)
- JS에서는 허용되나, TS에서 문제되는 경우가 있다. 이는 취향차이이다. (JS의 형 변환 특징에서 자유롭지 않음)
2 타입스크립트 설정 이해하기
noImplicitAny
- true일 경우, 명시적으로 any라고 선언하거나 더 분명한 타입을 사용한다.
- TS는 타입 정보를 가질 때 가장 효과적이므로, 되도록
noImplicitAny
를 사용하면 좋다.
// tsConfig: {"noImplicitAny":true}
function add(a, b) {
// ~ Parameter 'a' implicitly has an 'any' type
// ~ Parameter 'b' implicitly has an 'any' type
return a + b;
}
strictNullChecks
- null, undefined가 모든 타입에서 허용되는지 확인하는 설정
// tsConfig: {"noImplicitAny":true,"strictNullChecks":true}
const x: number = null;
// ~ null 형식은 number 형식에 할당할 수 없습니다.
명시적으로 의도를 드러내자
// tsConfig: {"noImplicitAny":true,"strictNullChecks":true}
const x: number | null = null;
- 코드 작성이 어렵다.
- 이 설정을 쓰기로 결정하였다면, noImplicitAny 설정부터 한다.
- 프로젝트가 커질수록 이 설정을 처음부터 하는 게 좋다.
노트
이 모든 설정을 하고 싶다면 strict
설정을 하라
3 코드 생성과 타입이 관계없음을 이해하기
타입스크립트 컴파일러의 역할
- 최신 ts, js를 브라우저에서 동작할 수 있도록 구버전의 자바스크립트로 트랜스파일 한다.
- 코드의 타입 오류 체크
타입 오류가 있는 코드도 컴파일이 가능
타입스크립트의 오류는 warning이나 다름없다. 문제되는 부분을 알려주지만 빌드를 못하는 건 아니다.
오류가 있을 때 컴파일하지 않으려면 noEmitOnError
를 설정하자.
노트
코드에 오류가 있을 때 '컴파일에 문제가 있다'라고 하는 게 아니라, '타입 체크에 문제가 있다'라고 하는 게 맞다.
런타임에는 타입 체크가 불가능
interface Square {
width: number;
}
interface Rectangle extends Square {
height: number;
}
type Shape = Square | Rectangle;
function calculateArea(shape: Shape) {
if (shape instanceof Rectangle) {
// ~~~~~~~~~ 'Rectangle' only refers to a type,
// but is being used as a value here
return shape.width * shape.height;
// ~~~~~~ Property 'height' does not exist
// on type 'Shape'
} else {
return shape.width * shape.width;
}
}
instanceof
는 런타임에 일어나지만,Rectangle
은 타입이므로 런타임 시점에 아무런 역할을 할 수 없음- 런타임에 타입 정보를 유지해야 함
속성 체크
interface Square {
width: number;
}
interface Rectangle extends Square {
height: number;
}
type Shape = Square | Rectangle;
function calculateArea(shape: Shape) {
if ('height' in shape) {
shape; // 타입이 Rectangle
return shape.width * shape.height;
} else {
shape; // 타입이 Square
return shape.width * shape.width;
}
}
태그 기법
interface Square {
kind: 'square';
width: number;
}
interface Rectangle {
kind: 'rectangle';
height: number;
width: number;
}
// 태그된 유니온(tagged union)
type Shape = Square | Rectangle;
function calculateArea(shape: Shape) {
if (shape.kind === 'rectangle') {
shape; // Type is Rectangle
return shape.width * shape.height;
} else {
shape; // Type is Square
return shape.width * shape.width;
}
}
- kind 로 체크 중...
클래스
타입(런타임 접근 불가)과 값(런타임 접근 가능)을 둘 다 사용하기
class Square {
constructor(public width: number) {}
}
class Rectangle extends Square {
constructor(public width: number, public height: number) {
super(width);
}
}
type Shape = Square | Rectangle;
function calculateArea(shape: Shape) {
if (shape instanceof Rectangle) {
shape; // Type is Rectangle
return shape.width * shape.height;
} else {
shape; // Type is Square
return shape.width * shape.width; // OK
}
}
- Rectangle를 클래스로 선언하면 타입과 값으로 모두 사용할 수 있으므로 오류가 없다.
type Shape = Square | Rectangle;
에서 Rectangle는 타입으로 참조되나,shape instanceof Rectangle
는 값으로 참조된다.
타입 연산은 런타임에 영향을 주지 않음
function asNumber(val: number | string): number {
return val as number;
}
타입 단언문
으로 인해 타입체커를 통과하나 이는 잘못된 코드이다. string에 대한 타입 가드가 되어있지 않기 때문.
function asNumber(val: number | string): number {
return typeof val === 'string' ? Number(val) : val;
}
런타임 타입은 선언된 타입과 다를 수 있음
function setLightSwitch(value: boolean) {
switch (value) {
case true:
turnLightOn();
break;
case false:
turnLightOff();
break;
default:
console.log(`I'm afraid I can't do that.`);
}
}
- console.log 가 실행될 여지가 있을까?
- boolean은 런타임에 제거된다.
- 매개변수에 boolean이 아닌 다른 값을 넣었다면...?
타입스크립트 타입으로는 함수를 오버로드 할 수 없음
함수 오버로딩: C++는 동일한 이름에 매개변수만 다른 여러 버전의 함수를 허용한다.
- 타입스크립트에서는 타입과 런타임의 동작이 무관하기 때문에 함수 오버로딩은 불가능하다.
- 기능은 지원하지만, 온전히 타입 수준에서만 동작한다.
타입스크립트 타입은 런타임 성능에 영향을 주지 않음
- JS 변환 시점에 제거되므로 런타임 성능에 영향이 없다.