타입스크립트에 대해 알아보기 전에...! 컴파일과 런타임의 개념을 먼저 알고 들어가보자 🙌
컴파일 👉 런타임
컴파일
해당 과정을 통해 컴퓨터가 인식할 수 있는 기계어 코드로 변환되어 실행 가능한 프로그램으로 만드는 시점이다.런타임
컴파일 과정을 마친 프로그램이 사용자에 의해서 실행되는 시점이다.
타입스크립트는 자바스크립트의 상위집합이다.
즉, 모든 자바스크립트 프로그램은 이미 타입스크립트 프로그램이다.
또한, 타입스크립트는 런타임 오류를 발생시키는 코드를 찾아내는 타입 시스템
을 가지고 있다.
이는 자바스크립트 동작을 모델링하는데 사용된다
타입스크립트의 파일 확장자는 .ts
또는 .tsx
를 사용한다
그런데, 자바스크립트의 파일의 확장자를 타입스크립트의 확장자로 바꾸면 이는 migration
이 된다.
모든 자바스크립트는 타입스크립트이지만, 모든 타입스크립트는 자바스크립트가 아니다.
console.log('Hello TypeScript');
function person(name: string) {
console.log(name);
}
console.log(person('seohee'));
export { }
또한, 타입스크립트의 타입 체커는 문제점을 찾아내기도 한다.
let city = 'new york city';
console.log(city.toUppercase()); //toUpperCase를 사용하시겠습니까??
여기서 타입체커를 짚고 넘어가면,
모든 자바스크립트는 타입스크립트이지만, 일부
자바스크립트만이 타입체커를 통과한다.
자바스크립트 프로젝트를 타입스크립트로 전환하는 것이 아니라면
noImplicit Any
를 설정하는 것이 좋으며, 보다 엄격한 체크를 위해서는strict
설정을 고려하는 것이 좋다
function add(a, b) {
return a + b;
}
add(10, null);
이를 실행하면
'a' 매개 변수에는 암시적으로 'any' 형식이 포함됩니다.
'b' 매개 변수에는 암시적으로 'any' 형식이 포함됩니다.
라는 오류가 발생된다,
따라서 우리는 타입스크립트의 타입을 명시적으로 선언해주는 것이 좋다
예를 들면,
function add(a: number, b: number) {
return a + b;
}
add(10, 2);
이런식으로 말이다.
따라서, 이를 코드 작성을 하면서 체크하기 위해서 tsc --init
명령어를 통해서 tsconfig.json
파일을 작성하는 것이 좋다
noImplicitAny
코드를 작성할 때 타입을 지정했는지 검사해주는 설정이다.
strictNullchecks
null undefined가 모든 타입에서 허용되는지 확인하는 설정이다.
noImplicitAny를 반드시 먼저 설정한 후에 진행해야 한다.
타입스크립트의 설정은 tsconfig.json을 이용하는 것이 좋고, 자바스크립트 프로젝트를 타입스크립트로 변환하는 것이 아니라면 noImplicit을 설정하는 것이 좋다.
타입스크립트 컴파일러는 2가지 역할을 수행한다.
- 자바스크립트로 트랜스파일한다.
- 코드의 타입 오류를 체크한다.
컴파일은 타입 체크와 독립적으로 동작하기 때문에, 만약에 타입 오류가 있어도 일단 컴파일은 가능
하다.
만약, 오류가 있을 때 컴파일하지 않으려면 tsconfig.json에 noEmitOnError
를 설정하면 된다.
👇 예제 코드를 살펴보자.
interface Square {
width: number;
}
interface Rectangle extends Square {
height: number;
}
type Shape = Square | Rectangle;
// 변수 : 타입
function calculateArea(shape: Shape) {
if (shape instanceof Rectangle) {
// ~~~~~~~~~ 'Rectangle' 은 형식만 참조하지만
// 여기서는 값으로 사용되고 있습니다
return shape.width * shape.height;
// ~~~~~~ 'Shape' 형식에 'height'속성이 없습니다
} else {
return shape.width * shape.width;
}
}
interface
와 type
은 자바스크립트로 컴파일 되는 과정에서 제거되기 때문에 Rectangle
은 타입의 역할이 아닌 값
으로 수행된다.
결론
타입은 런타임에 접근 불가능하지만, 값은 런타임에 접근 가능한 것이다.
👇 따라서 이를 타입의 유무가 아닌, 속성의 존재 유무
를 사용해야 한다.
function calculateArea(shape: Shape) {
// 'height' 속성의 유무 파악
if ('height' in shape) {
shape; // 타입이 Rectangle
return shape.width * shape.height;
} else {
shape; // 타입이 Square
return shape.width * shape.width;
}
}
👇 또 다른 방법으로는 태그 기법
이 있다.
function calculateArea(shape: Shape) {
if (shape.kind === 'rectangle') {
shape;
return shape.width * shape.height;
} else {
shape;
return shape.width * shape.width;
}
}
여기서 Shape 타입은 태그된 유니온
을 사용하였으며, 이는 런타임에 타입 정보를 쉽게 유지하고자 사용되는 방법이다.
사실, interface
를 사용하면서 혼란스러운 상황이 야기되었다고 할 수 있다.
👇 타입과 값을 모두 사용하는 기법인 class
로 사용하면 문제가 해결된다.
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;
return shape.width * shape.height;
} else {
shape;
return shape.width * shape.width;
}
}
type Shape = Square | Rectangle
Rectangle은 타입으로 참조되지만, if (shape instanceof Rectangle)
Rectangle은 값으로 참조된다.
👇 아래의 코드는 타입 체커를 통과하지만 잘못된 코드이다.
function asNumber(val: number | string): number {
return val as number;
}
타입스크립트를 자바스크립트로 변환한 코드이다.
function asNumber(val){
return val;
}
위를 토대도 코드에 어떠한 정제 과정도 없다는 것을 확인할 수 있다.
타입스크립트 파일에서 as number
는 타입 연산이지만, 런타임 동작에서는 작동하지 않는다.
따라서 값을 정제하기 위해서는 런타임의 타입을 체크해야하고 이는 자바스크립트 연산으로 변환을 시켜야한다.
function asNumber(val: number | string):number{
// return typeof(val) === 'string' ? Number(val) : val;
return Number(val);
}
다른 언어와 달리 타입스크립트는 타입과 런타임의 동작 관계가 없기 때문에,
함수 오버로딩
이 불가능하다.
함수 오버로딩 기능 자체는 지원하지만, 온전히 타입 수준에서만 동작한다.
즉, 하나의 함수에 대해 여러 개의 선언문을 작성할 수는 있지만 실제 구현체는 단 하나
뿐이다.
👇 구현체가 중복으로 작성된 코드이다.
function add(a: number, b: number) { return a + b; }
// 중복된 함수 구현
function add(a: string, b: string) { return a + b; }
// 중복된 함수 구현
👇 이러한 오류를 방지하기 위해 선언문을 사용해야 한다.
// tsConfig: {"noImplicitAny":false}
function add(a: number, b: number): number;
function add(a: string, b: string): string;
function add(a, b) {
return a + b;
}
const three = add(1, 2); // 타입 -> number
const twelve = add('1', '2'); // 타입 -> string
결론
타입스크립트의 타입은 런타임 동작과 성능에 무관하며, 타입 오류가 있더라고 일단 컴파일(코드 생성)은 가능하다.
하지만, 타입을 런타임에 사용할 수 없기 때문에타입 정보 유지
를 위해서는유니온과 속성 체크 방법, 클래스의 방법
을 사용해야 한다.
자바스크립트는 객체가 어떤 타입에 맞는 변수와 메서드를 가지면, 해당 타입에 속하는 객체로 자연스럽게 여기는 덕 타이핑 기반 언어이다.
타입스크립트는 매개변수 값이 주어진 조건에 부합하면 무엇인지 신경 쓰지 않는 동작을 그대로 반영하기 때문에, 가끔은 예상치 못한 결과를 초래하기도 한다.
구조적 타이핑을 통해 견고한 코드를 작성하도록 해보자.
👇 예제 코드를 살펴보자.
interface Vector2D {
x: number;
y: number;
}
interface NamedVector {
name: string;
x: number;
y: number;
}
function calculateLength(v: Vector2D) {
return Math.sqrt(v.x * v.x + v.y * v.y);
}
const v: NamedVector = { x: 3, y: 4, name: 'Zee' };
calculateLength(v); // 5
NamedVector는 x,y의 속성을 가지고 있기에 Vector2D와 호환되어 정상적으로 calculateLength를 작동시킨다. 이것이 구조적 타이핑이다.
하지만, 구조적 타이핑 때문에 오류를 발생하기도 한다.
interface Vector3D {
x: number;
y: number;
z: number;
}
function normalize(v: Vector3D) {
const length = calculateLength(v);
return {
x: v.x / length,
y: v.y / length,
z: v.z / length,
};
}
normalize({ x : 3, y : 4, z : 5});
// { x : 0.6, y : 0.8, z : 1}
Vector3D와 호환되는 객체로 calculateLength를 호출하게 되면, z가 정규화 과정에서 무시된다.
👇 이를 위해 Vector3D 타입의 매개변수를 가진 함수를 호출해야 한다.
function calculateLengthL1(v: Vector3D) {
return Math.abs(v.x) + Math.abs(v.y) + Math.abs(v.z);
}
결론
타입스크립트에서의 타입은 봉인되어 있지 않기에 구조적 타이핑을 이해해야 하고, 이를 통해 유닛 테스팅을 작성하려는 노력이 필요하다.
타입스크립트의 코드에는 타입을 조금씩 추가할 수도, 언제든지 제거할 수도 있기 때문에 점진적이고 선택적이다.
하지만, any타입을 사용하게 된다면 타입스크립트의 장점을 가지지 못하기 때문에 부득이하게 사용하게 되더라도 any타입이 가지는 위험성을 반드시 인지하고 있어야 한다.
age = '12' as any;
age += 1; // "121"
자바스크립트와 타입스크립트의 관계를 알아보고, 타입스크립트에서의 타입이 어떤 식으로 작동되는지 알아보았다. 아직 초반이라 타입스크립트를 더 다뤄보고와서 다시 이 글을 수정할 예정이다.
📚 학습할 때, 참고한 자료 📚