$ npm install -g typescript
$ tsc app.ts
$ tsc app # 확장자 생략 가능
# JS로 컴파일된 app.js 파일 생성
// class.ts
class JsObject {
constructor(name: string) {
this.name = name // 클래스 몸체에 프로퍼티 선언이 없어 에러 발생
}
}
// class.js
class JsObject {
constructor(name) {
this.name = name; // 생성자 내에 프로퍼티 선언을 하는 것이 정상이므로 에러 발생 X
}
}
컴파일중 에러가 발생한더라도 컴파일된 JS 파일이 생긴다. 그 이유는 TS에서는 문제가 있는 코드라도 JS에서는 문제가 없는 경우가 있기 때문이다. 위의 예제가 그 예시이다
--noEmitOnError
옵션을 통해 컴파일 중 에러 발생시 결과물이 생성되지 않게할 수 있다.
$ tsc --noEmitOnError class.ts
위의 예제를 tsc class.ts
명령어를 통해 컴파일한 결과는 사실 다르다
// 실제 결과물
var JsObject = /** @class */ (function () {
function JsObject(name) {
this.name = name;
}
return JsObject;
}());
기본적으로 tsc는 es3로 컴파일되도록 되어있다. 하지면 현재 대다수의 브라우저는 es3 이상의 ECMA 표준을 지원하므로 --target
명령어를 통해 표준을 지정할 수 있다
$ tsc --target es2015 class.ts
위의 예제의 클래스 문법을 사용한 JS 파일이 생성되는 것을 볼 수 있다
암묵적으로 any
타입으로 추론되는 경우 에러를 발생시킨다
$ tsc --noImplicitAny class.ts
null 또는 undefined 검사를 강제하는 옵션이다
$ tsc --strictNullChecks class.ts
--noImplicitAny
, --strictNullChecks
옵션은 TS의 엄격도 레벨을 올리고 프로그래머의 실수를 미연에 방지할 수 있는 옵션이다
function greet(name: string): void {
console.log(`Hello, ${name}`)
}
function greetImplicit(name) {
console.log(`Hello, ${name}`)
}
명시적으로 매개 변수의 타입을 표기하지 않으면 any
로 타입을 추론한다. 단 noImplictAny
옵션이 활성화 되어있는 경우 에러가 발생하니 주의
반환 타입의 경우 return 문을 통해 타입을 추론한다. 반환이 없는 경우 void
타입으로 추론
익명 함수의 매개 변수 타입은 문맥을 통해 any
가 아닌 타입으로 추론하게된다
const fruits = ['apple', 'banana', 'tomato'] // string[] 타입
fruits.map((f) => { // f를 string 타입으로 추론
console.log(f.toUpperCase())
})
map
함수를 통해 넘겨받는 매개변수를 문맥을 통해 string
타입으로 추론해 IDE의 자동완성 기능이 작동하는 것을 볼 수 있다
이름만 같고 매개변수의 개수나 타입이 다른 여러개의 메소드가 존재하는 다른 언어의 오버로딩과 달리 자바스크립트는 이름이 같은 함수는 단 한개만 존재할 수 있다. 따라서 타입스크립트의 오버로딩은 이름이 같고 매개변수와 반환값등의 시그니처가 다른 함수를 정의만 해두고 모든 정의를 아우르는 구현부가 한개 존재하는 방식으로 오버로딩이 가능하다
따라서 함수별 로직이 서로 많이 다르다면 이름을 다르게 함수를 구현하는 것이 낫고 서로 로직이 겹칠 경우 오버로딩하여 구현할 수 있다.
// 오버로딩 이전
function formatDate(date: Date, format = "yyyyMMdd"): string {
const yyyy = date.getFullYear().toString();
const MM = addZero(date.getMonth() + 1);
const dd = addZero(date.getDate());
return format.replace("yyyy", yyyy).replace("MM", MM).replace("dd", dd);
}
function formatDateString(dateStr: string, format = "yyyyMMdd"): string {
const date = new Date(dateStr);
const yyyy = date.getFullYear().toString();
const MM = addZero(date.getMonth() + 1);
const dd = addZero(date.getDate());
return format.replace("yyyy", yyyy).replace("MM", MM).replace("dd", dd);
}
function formatDateTime(datetime: number, format = "yyyyMMdd"): string {
const date = new Date(datetime);
const yyyy = date.getFullYear().toString();
const MM = addZero(date.getMonth() + 1);
const dd = addZero(date.getDate());
return format.replace("yyyy", yyyy).replace("MM", MM).replace("dd", dd);
}
// 오버로딩 적용 후
// 시그니처 정의
function formatDate(date: Date, format: string): string;
function formatDate(date: number, format: string): string;
function formatDate(date: string, format: string): string;
// 오버로딩 구현
function formatDate(date: Date | number | string, format = "yyyyMMdd"): string {
const dateObj = new Date(date);
const yyyy = dateObj.getFullYear().toString();
const MM = addZero(dateObj.getMonth() + 1);
const dd = addZero(dateObj.getDate());
return format.replace("yyyy", yyyy).replace("MM", MM).replace("dd", dd);
}
인덱서블 타입은 인덱싱 할때 해당 반환 유형과 함께 객체를 인덱싱하는 데 사용할 수 있는 타입을 기술하는 인덱스 시그니처(index signature)를 가지고 있다
인덱스 시그니처의 타입은 string
, number
만 가능하고 둘을 동시에 사용하는 것도 가능하다. 단 두개의 인덱스 시그니처를 같이 정의할 경우 number
시그니처의 타입은 string
시그니처 타입과 같거나 서브타입이어야 한다.
number
타입의 프로퍼티는 인덱싱시 string
으로 변환되어 사용되기 때문에 사실 같다고 볼 수 있다. 따라서 타입의 일관성이 필요
class Animal { }
class Dog extends Animal { }
interface Foo {
[a: string]: Dog,
[b: number]: Animal // 에러 발생. Animal은 Dog의 서브 타입이 아닌 부모 타입이기 때문
}
마찬가지로 인덱스 시그니처가 존재하는 경우 인덱스 시그니처의 키 타입과 같은 일반 프로퍼티의 타입도 인덱스 시그니처와 일치해야한다
interface Foo {
[key: number]: string
10: number // 에러 발생. 인덱스 시그니처의 값 타입과 일치하지 않기 때문
}
객체 리터럴을 다른 변수에 할당할 때나 인수로 전달할 때, 특별한 처리를 받고 초과 프로퍼티 검사
를 받는다. 초과 프로퍼티 검사란 인터페이스의 정의되지 않은 프로퍼티를 허용하지 않고 에러를 발생하도록 하는 검사이다
interface LabeledValue {
label: string;
}
function printLabel(labeledObj: LabeledValue) {
console.log(labeledObj);
}
const myObj = {label: 'size 10 object', size: 10}
printLabel(myObj)
printLable()
함수의 인자로 LabeledValue
인터페이스보다 범위가 큰 객체를 넘겼다. 이 코드는 에러가 발생하지 않는다. 객체 리터럴이 아닌 변수를 인수로 전달하였기 때문에 초과 프로퍼티 검사가 일어나지 않기 때문이다
interface LabeledValue {
label: string;
}
function printLabel(labeledObj: LabeledValue) {
console.log(labeledObj);
}
const myObj: LabledValue = {label: 'size 10 object', size: 10} // 변수 할당 시 초과 프로퍼티 검사에 따른 에러 발생
printLabel(myObj)
// 또는
printLabel({label: 'size 10 object', size: 10}) // 객체 리터럴을 인자로 넘겨 초과 프로퍼티 검사에 따른 에러 발생
위의 예제와 같이 변수 할당시 타입을 지정하거나 객체 리터럴을 인자로 전달하면 초과 프로퍼티 검사가 작동하여 에러가 발생한다
타입 단언을 사용하면 초과 프로퍼티 검사를 무력화할 수 있다
interface LabeledValue {
label: string;
}
function printLabel(labeledObj: LabeledValue) {
console.log(labeledObj);
}
printLabel({label: 'size 10 object', size: 10} as LabeledValue) // 에러 발생 X
하지만 초과 프로퍼티 검사를 무력화하는 것은 좋지 않다. 전달된 매개변수를 통해서는 인터페이스에 정의된 프로퍼티 외에 다른 변수에 접근하는 것은 불가능하지만 여러 방법을 통해 접근할 수 있기 때문이다. 즉 문제를 불러올 수 있으므로 가능한 사용하지 않거나, 인덱스 시그니처를 통해 정의된 프로퍼티 외의 프로퍼티를 정의하는 것이 좋다
interface LabeledValue {
label: string;
}
function printLabel(labeledObj: LabeledValue) {
for(let key in labeledObj) {
console.log(key, labeledObj[key])
}
}
const myObj = {label: 'size 10 object', size: 10}
printLabel(myObj)
/** 출력
* label size 10 object
* size 10 // 인터페이스 외 프로퍼티에 접근
*/
위의 예제를 보면 --noImplicitAny
옵션이 활성화 되어 있지 않다면 에러가 발생하지 않는다. 이러한 문제점이 있음을 생각해야한다
class Control {
private state: any;
}
interface SelectableControl extends Control {
select(): void;
}
class Button extends Control implements SelectableControl {
select() { }
}
// Error: 'Foo' 클래스가 'SelectableControl' 인터페이스를 잘못 구현합니다.
// 형식에 별도의 프라이빗 속성 'state' 선언이 있습니다.
class Foo1 implements SelectableControl {
private state: any;
select() { }
}
// Error: 'Foo2' 클래스가 'SelectableControl' 인터페이스를 잘못 구현합니다.
// 'Foo2' 형식에 'SelectableControl' 형식의 state 속성이 없습니다.
class Foo2 implements SelectableControl {
select() { }
타입스크립트의 인터페이스는 클래스를 확장할 수 있다. 일반적인 인터페이스는 프로퍼티가 읽기 전용인지 정도만 설정할 수 있지만 클래스를 확장할 경우 private
, public
과 같은 접근 지정자까지 상속받는다. 또한 클래스를 확장한 인터페이스를 구현하는 경우 해당 클래스를 상속하지 않는 경우 에러가 발생하므로 클래스의 상속을 강제할 수 있다.
왜 이렇게 쓰는지는 이해하지 못했지만 이런 방식의 사용이 가능하다고 알고 있으면 될 것 같다
모듈은 전역 스코프가 아닌 자체 스코프 내에서 실행된다, 즉 모듈 내에서 선언된 변수, 함수, 클래스, 인터페이스, 타입 별칭 등은 명시적으로 export
하지 않는 한 모듈 외부에서 보이지 않습니다. 반대로 다른 모듈에서 export
한 변수, 함수, 클래스, 인터페이스 등을 사용하기 위해서는 import
를 통해 가져와야 한다.
만약 파일 내에 export
, import
가 존재하지 않으면 해당 파일은 전역 스코프를 가지게 되므로 유의해햐 한다
export class Foo { }
export const bar = 'Bar'
class Foo { }
export { Foo }
export { Foo as Bar } // 이름을 바꿔 export
import { Foo } from './foo.ts'
import { Bar as Baz } from './bar.ts'
import * as Foo from './foo.ts'
모듈의 export와 관계없이 발생하는 부수 효과만을 가져오는 import
// side.ts
console.log('side module')
// runtime.ts
import './side.ts'
console.log('runtime')
/** runtime.ts 실행 결과
* side module // 부수 효과 실행
* runtime
*/
// 암시적 import
import { Foo } from "./foo.ts";
// 명시적으로 import type을 사용하기
import type { Foo } from "./foo.ts";