.js
혹은 .jsx
이지만 타입스크립트 파일의 확장자 명은 .ts
혹은 .tsx
이다. 하지만 두 언어는 완전히 다른 체계의 언어가 아니라는 점을 유의하자..js
확장자를 .ts
로 변경한다고 해도 달라지는 것은 없다.function greet(who: string): string {
console.log(`Hello, ${who}!`);
}
: string
같은 문법은 TS에서 사용되는 문법이지, JS에는 없는 문법이기 때문이다.let city = 'new york city`
// Uncaught TypeError: city.toUppercase is not a function
// TS는 자동 추론을 통해 city 변수의 타입을 string 으로 인식한다.
console.log(city.toUppercase());
TypeError
가 발생한다.const nations = [
{ name: 'Japan', capital: 'Tokyo' },
{ name: 'Korea', capital: 'Seoul' },
];
for (const nation of nations) {
// JS : undefined, undefined 출력
// TS : 컴파일 과정에서 capitol 속성이 없음을 명시
console.log(state.capitol);
}
interface Nation {
name: string;
capital: string;
}
const nations: Nation[] = [
{ name: 'Japan', capital: 'Tokyo' },
{ name: 'Korea', capitol: 'Seoul' }, // '{ name: string; capitol: string; }' 형식은 'Nation' 형식에 할당할 수 없습니다.
];
for (const nation of nations) {
// 만약 Nation 인터페이스를 선언하여 사용하지 않았더라면?
// TS는 오류를 내뿜지 않고 그대로 코드를 실행했을 것이다.
console.log(state.capitol);
}
nations
배열 내부의 인자는 반드시 Nation
인터페이스의 양식을 따르도록 TS에게 의도를 전달해준 케이스이다.// TS는 컴파일 과정에서 하단의 코드에 오류를 지적하지 않는다.
const x = 2 + '3';
const y = '2' + 3;
// TS 는 컴파일 과정에허 하단의 코드에 오류를 지적한다.
const a = null + 7; // 여기서는 null 을 사용할 수 없습니다.
const b = [] + 12; // '+' 연산자를 'never[]' 및 'number' 형식에 적용할 수 없습니다. 왜냐면 현재 [] 는 값 영역에 있기 때문에 수정의 여지가 없어 never[] 로 추론된 상태이다.
const b = []; // b는 any[] 로 추론된다, b가 가리키는 주소의 값은 배열이지만, 내부에 어떤 타입의 요소들이라도 다 넣을 수 있기 때문에 any[] 로 추론된다.
const name = ['Baik', 'Kim'];
// index 의 유효 범위가 넘어갈 것이라는 생각까지는 TS에서 해주지 않는다.
console.log(name[2].toUpperCase());
name
배열을 사용할 때 유효한 index 내에서 사용할 것이라 생각했지만, 실제로는 그렇지 않았기 때문이다.tsconfig.json
파일 내부에서 설정을 제어하며, tsc --init
명령어를 통해 생성이 가능하다.// noImplicitAny 옵션이 꺼져 있다면, a외 b는 any로 추론된다.
function add(a, b) {
return a + b;
}
add(10, null);
noImplicitAny
설정은 TS가 암시적으로 타입을 any
로 추론하지 않도록 하는 옵션이다. TS는 타입 정보를 가졌을 때가 가장 효율적이기에, 되도록이면 noImplicitAny
설정을 켜두어야 한다.// strictNullChecks 옵션이 꺼져 있다면, x에는 정상적으로 null이 할당된다.
const x: number = null;
// 사용자가 직접 타입을 지정하여 해당 변수에 null 도 할당될 수 있음을 명시.
const y: number | null = null;
strictNullChecks
옵션은 null
과 undefined
가 모든 타입에서 허용되는지를 체크하는 속성이다.const element = document.getElementById('status');
element.innerText = 'Done!'; // element는 null 일 수 있습니다.
// 별도의 타입 가드를 통해 element가 null 이 아님을 체크해야 한다.
if (element) {
element.innerText = 'Done!';
}
// 혹은 다음과 같이 Type Assertion 을 활용한 타입 단언으로도 가능하다.
const element = document.getElementById('status') as HTMLElement;
element.innerText = 'Done!'; // TS에서는 더 이상 null 인 케이스를 고려하지 않는다.
strictNullChecks
옵션은 코드의 작성을 가끔 어렵게 한다. TS에게 해당 값이 어디서부터 왔으며 가끔 필요하다면 as
키워드를 통한 타입 단언도 진행해주어야 한다.null
과 undefined
에 대한 잠재적 위험도가 높아지기에 초기에 이러한 설정을 엄격히 잡아내면서 개발하는 것이 더욱 효율적이다.따라서 컴파일 과정 에서 문제가 발생했다는 말은 틀렸다.
noEmitOnError
옵션을 설정하면 된다.interface Square {
width: number;
}
interface Rectangle extends Square {
height: number;
}
type Shape = Square | Rectangle;
function calculateArea(shape: Shape) {
// 'Shape' 은(는) 형식만 참조하지만, 여기서는 값으로 사용되고 있습니다.
if (shape instanceof Square) {
return shape.width ** 2;
}
return shape.width * shape.height;
}
instanceof
는 런타임에 일어나지만 Square
은 타입이기에 런타임 시점에서는 아무런 역할을 할 수 없다. TS의 타입은 JS로 컴파일 되는 과정에서 모두 제거 되기 때문이다.shape
변수의 타입을 명확히 하기 위해서는 런타임에서 타입에 대한 정보를 유지하는 방법이 필요하다.function calculateArea(shape: Shape) {
// 해당 조건식이 참이라면, TS는 shape를 자동으로 Square로 추론한다.
if (!'height' in shape) {
return shape.width ** 2;
}
// 조건식이 거짓이라면 TS는 shape를 Rectangle로 자동 추론한다.
return shape.width * shape.height;
}
interface Square {
kind: 'square';
width: number;
}
interface Rectangle {
kind: 'rectangle';
width: number;
height: number;
}
type Shape = Square | Rectangle;
function calculateArea(shape: Shape) {
// kind 속성의 값 유무에 따라 shape 변수의 타입을 추론한다.
if ((shape.kind = 'square')) {
return shape.width ** 2;
}
return shape.width * shape.height;
}
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) {
// 클래스 인스턴스를 체크하는 것이기에 instanceof 사용이 가능해진다.
// 조건식이 참이라면 TS는 자동으로 shape를 Square 타입으로 추론한다.
if (shape instanceof Square) {
return shape.width ** 2;
}
return shape.width * shape.height;
}
interface LightApiResponse {
lightSwitchValue: boolean; // boolean | string 이라면?
}
async function setLight() {
const response = await fetch('/light');
const result: LightApiResponse = await response.json();
setLightSwitch(result.lightSwitchValue);
}
fetch('/light')
의 응답이 반드시 LightApiResponse
를 반환할 것이라는 보장이 없다. 만약 lightSwitchValue
속성이 boolean이 아닌 string 타입이어도 코드는 실행이 된다.function add(a: number, b: number) {
return a + b;
}
// 중복된 함수 구현이다. 오버로딩이 아닌 에러를 일으킨다.
function add(a: string, b: string) {
return Number(a) + Number(b);
}
// TS로 작성된 asNumber 함수, 컴파일 단계에서는 잘 작동되지만..
function asNumber(val: number | string): number {
return val as number;
}
// 컴파일 후 변환된 코드는 아래와 같이 타입 체크가 삭제되어 있다.
function asNumber(val) {
return val;
}
// 런타임 과정에서 실제 val의 타입을 체크하고, 그에 맞는 로직을 구현
function asNumber(val: number | string): number {
return typeof val === 'string' ? Number(val) : val;
}
덕 타이핑, 혹은 구조적 서브 타이핑 이란?
interface Vector2D {
x: number;
y: number;
}
interface NamedVector {
name: string;
x: number;
y: number;
}
function calculateLength(v: Vector2D) {
return Math.sqrt(v.x ** 2 + v.y ** 2);
}
const nv = { name: 'Test', x: 3, y: 4 }; // NamedVector
const nvLength = calculateLength(nv); // 5
NamedVector
는 Vector2D
의 속성인 x
와 y
를 모두 가지고 있기에 calculateLength 함수로 호출이 가능하다. TS에서는 두 인터페이스 간의 구조가 호환되는지를 자동으로 체크해준다.NamedVector
와 Vector2D
는 서로 다른 인터페이스이기 때문에 호환이 안된다고 생각하겠지만, 실제로는 호환이 가능하기 때문에 정상적으로 함수가 호출된다.NamedVector
는 Vector2D
의 상속 관계를 굳이 선언하지 않아도 함수에서 호출이 가능하다. TS는 JS의 런타임 동작을 모델링 하기에 런타임 과정에서는 JS에서 문제가 되지 않는 코드라면 정상적으로 작동되는 것이다.타입은 봉인되어 있지 않음 을 반드시 유의하자.
interface Vector3D {
x: number;
y: number;
z: number;
}
const v = { x: 4, y: 3, z: 5 };
function normalize(v: Vector3D) {
const length = calculateLength(v);
return { x: v.x / length, y: v.y / length, z: v.z / length };
}
Vector2D
만을 받는 calculateLength 함수를 사용했음에도 문제없이 코드가 실행되었다.Vector3D
와 호환되는 객체로 해당 함수를 호출하면, 구조적 타이핑 관점에서 Vector2D
와 호환이 된다. x
와 y
속성을 모두 갖추었기 때문이다.function calculateLengthL1(v: Vector3D) {
let length = 0;
for (const axis of Object.keys(v)) {
// `string` 은 `Vector3D` 의 인덱스로 사용할 수 없기에, 엘리먼트는 암시적으로 any 입니다.
const coord = v[axis];
length += Math.abs(coord);
}
return length;
}
axis
는 Vector3D
의 속성인 x, y, z
중 하나만을 가질 것이라 생각했지만, 사실 매개변수 v
는 x, y, z
뿐만 아니라 다른 속성을 가지고 있을 수도 있다.const v3D = { x: 3, y: 4, z: 5, address: 'Hwaseong' };
calculateLengthL1(v3D); // NaN
v
는 어떤 속성이던 가질 수 있기 때문에 axis
의 타입은 string
도 될 수 있다. 따라서 TS는 v[axis]
의 속성을 number 라고 확정지을 수가 없는 상태이다.address
속성이 들어간다면? 이는 그대로 런타임 과정에서 실행되어 NaN
이라는 결과를 발생시킬 것이다.any
로 언제든지 해제할 수 있기 때문이다.any
를 사용하면 TS에서 제공하는 강력한 장점들을 누리기 어렵다. 따라서 사용을 최대한 자제하는 것이 좋다.let age: number;
age = '12' as any; // 허용이 된다. 하지만 좋지 못하다.
age
변수는 number 타입으로 선언되었지만 이후 any
타입으로 단언되어 string
도 할당이 가능하게 되었다. 이렇게 될 경우 타입 체커가 올바르게 작동하지 못하게 된다.any
타입을 사용할 경우 잘 지켜지지 않게 된다.function calculateAge(birth: Date): number {
return ...
}
let birthDate: any = '1990-01-19'
calculateAge(birthDate) // 코드는 정상적으로 실행된다.
calculateAge
는 인자를 Date
타입으로 받아야 하나 any
는 이러한 제약 조건을 깡그리 무시하고 함수를 실행시켜 결과 값을 받아낸다.any
타입은 이러한 도움을 받지 못한다.any
를 사용하게 된다면 어느 시점부터 문제가 되는 값이 들어오게 되었는지 체크하는 것이 사실상 불가능해진다.any
가 아닌 구체적인 타입을 사용했다면 예방이 가능하다.any
를 사용해버리면 해당 객체 내부의 속성을 감추기 때문에 추후 설계를 확인하기 어려워진다.any
타입을 사용한다는 것은 오히려 독이 될 수 있다.