JavaScript 파일 타입 검사
JavaScript 파일 타입 검사 (Type Checking JavaScript Files)
checkJs를 켜거나 JS 파일을 TypeScript 프로젝트에 포함하면, 컴파일러가 .js 파일의 타입까지 검사하기 시작해요. 다만 .ts 파일과 검사 방식에 몇 가지 주목할 만한 차이가 있습니다. 이 문서는 그 차이를 하나씩 정리해 드릴게요.
출처: TypeScript 핸드북
클래스 본문의 할당에서 프로퍼티가 추론됨
ES2015는 클래스에 프로퍼티를 선언하는 수단이 없어요. 프로퍼티는 객체 리터럴처럼 동적으로 할당됩니다.
.js 파일에서 컴파일러는 클래스 본문 안의 프로퍼티 할당으로부터 프로퍼티를 추론합니다. 프로퍼티의 타입은 생성자에 주어진 타입이며, 생성자에 정의되지 않았거나 생성자의 타입이 undefined 또는 null이면 예외적으로, 해당 할당들의 오른쪽 값 타입들의 합집합(union)이 됩니다. 생성자에 정의된 프로퍼티는 항상 존재한다고 간주되는 반면, 메서드·getter·setter에만 정의된 프로퍼티는 선택적(optional)으로 취급돼요.
// @checkJs
// @errors: 2322
class C {
constructor() {
this.constructorOnly = 0;
this.constructorUnknown = undefined;
}
method() {
this.constructorOnly = false;
this.constructorUnknown = "plunkbat"; // ok, constructorUnknown is string | undefined
this.methodOnly = "ok"; // ok, but methodOnly could also be undefined
}
method2() {
this.methodOnly = true; // also, ok, methodOnly's type is string | boolean | undefined
}
}
프로퍼티가 클래스 본문에서 결코 설정되지 않으면 알 수 없는(unknown) 것으로 간주됩니다. 클래스에 읽기만 하는 프로퍼티가 있다면, 생성자에서 JSDoc으로 타입을 지정하는 선언을 추가하고 주석을 달아 주세요. 나중에 초기화될 예정이라면 값까지 줄 필요는 없어요:
// @checkJs
// @errors: 2322
class C {
constructor() {
/** @type {number | undefined} */
this.prop = undefined;
/** @type {number | undefined} */
this.count;
}
}
let c = new C();
c.prop = 0; // OK
c.count = "string";
생성자 함수는 클래스와 동일
ES2015 이전에는 JavaScript가 클래스 대신 생성자 함수를 사용했어요. 컴파일러는 이 패턴을 지원하며, 생성자 함수를 ES2015 클래스와 동일하게 이해합니다. 앞서 설명한 프로퍼티 추론 규칙도 정확히 똑같이 적용돼요.
// @checkJs
// @errors: 2683 2322
function C() {
this.constructorOnly = 0;
this.constructorUnknown = undefined;
}
C.prototype.method = function () {
this.constructorOnly = false;
this.constructorUnknown = "plunkbat"; // OK, the type is string | undefined
};
CommonJS 모듈 지원
.js 파일에서 TypeScript는 CommonJS 모듈 형식을 이해할 수 있어요. exports와 module.exports에 대한 할당은 export 선언으로 인식됩니다. 마찬가지로 require 함수 호출은 모듈 import로 인식됩니다. 예를 들면:
// same as `import module "fs"`
const fs = require("fs");
// same as `export function readFile`
module.exports.readFile = function (f) {
return fs.readFileSync(f);
};
JavaScript의 모듈 지원은 TypeScript의 모듈 지원보다 문법적으로 훨씬 관대합니다. 대부분의 할당·선언 조합이 지원돼요.
클래스, 함수, 객체 리터럴은 네임스페이스
.js 파일에서 클래스는 네임스페이스예요. 이를 이용해 클래스를 중첩할 수 있습니다. 예를 들면:
class C {}
C.D = class {};
그리고 ES2015 이전 코드에서는 이를 이용해 정적 메서드를 흉내 낼 수 있어요:
function Outer() {
this.y = 2;
}
Outer.Inner = function () {
this.yy = 2;
};
Outer.Inner();
단순한 네임스페이스를 만드는 데도 쓸 수 있어요:
var ns = {};
ns.C = class {};
ns.func = function () {};
ns;
다른 변형들도 허용됩니다:
// IIFE
var ns = (function (n) {
return n || {};
})();
ns.CONST = 1;
// defaulting to global
var assign =
assign ||
function () {
// code goes here
};
assign.extra = 1;
객체 리터럴은 개방적(open-ended)
.ts 파일에서는 변수 선언을 초기화하는 객체 리터럴이 그 선언에 타입을 줘요. 원래 리터럴에 지정되지 않은 새 멤버는 추가할 수 없습니다. 이 규칙은 .js 파일에서 완화되는데, 객체 리터럴은 원래 정의되지 않은 프로퍼티의 추가와 조회를 허용하는 개방형 타입(인덱스 시그니처)을 가지게 됩니다. 예를 들어:
var obj = { a: 1 };
obj.b = 2; // Allowed
객체 리터럴은 마치 [x:string]: any 인덱스 시그니처가 있는 것처럼 동작해서, 닫힌 객체가 아니라 열린 맵으로 취급됩니다.
다른 특수 JS 검사 동작처럼, 이 동작도 변수에 JSDoc 타입을 지정하면 바꿀 수 있어요. 예를 들어:
// @checkJs
// @errors: 2339
/** @type {{a: number}} */
var obj = { a: 1 };
obj.b = 2;
null, undefined, 빈 배열 초기화자는 타입 any 또는 any[]
null이나 undefined로 초기화된 모든 변수·파라미터·프로퍼티는, strict null checks가 켜져 있어도 타입 any를 가집니다. []로 초기화된 모든 변수·파라미터·프로퍼티는 strict null checks가 켜져 있어도 타입 any[]를 가져요. 유일한 예외는 위에서 설명한 대로 여러 초기화자를 가진 프로퍼티입니다.
function Foo(i = null) {
if (!i) i = 1;
var j = undefined;
j = 2;
this.l = [];
}
var foo = new Foo();
foo.l.push(foo.i);
foo.l.push("end");
함수 파라미터는 기본적으로 선택적
ES2015 이전 JavaScript에는 파라미터의 선택 여부를 지정할 방법이 없으므로, .js 파일의 모든 함수 파라미터는 선택적(optional)으로 간주됩니다. 선언된 파라미터 수보다 적은 인자로 호출하는 것이 허용돼요.
인자가 너무 많은 함수를 호출하는 것은 오류라는 점이 중요합니다.
예를 들어:
// @checkJs
// @strict: false
// @errors: 7006 7006 2554
function bar(a, b) {
console.log(a + " " + b);
}
bar(1); // OK, second argument considered optional
bar(1, 2);
bar(1, 2, 3); // Error, too many arguments
JSDoc으로 주석이 달린 함수는 이 규칙에서 제외됩니다. 선택 여부를 표현하려면 JSDoc 선택적 파라미터 문법([ ])을 사용하세요. 예를 들면:
/**
* @param {string} [somebody] - Somebody's name.
*/
function sayHello(somebody) {
if (!somebody) {
somebody = "John Doe";
}
console.log("Hello " + somebody);
}
sayHello();
arguments 사용에서 가변 인자 파라미터 선언이 추론됨
본문에 arguments 참조가 있는 함수는 암시적으로 가변 인자(var-arg) 파라미터((...arg: any[]) => any)를 가진 것으로 간주됩니다. 인자의 타입을 지정하려면 JSDoc 가변 인자 문법을 사용하세요.
/** @param {...number} args */
function sum(/* numbers */) {
var total = 0;
for (var i = 0; i < arguments.length; i++) {
total += arguments[i];
}
return total;
}
지정되지 않은 타입 파라미터는 any로 기본값
JavaScript에는 제네릭 타입 파라미터를 지정하는 자연스러운 문법이 없으므로, 지정되지 않은 타입 파라미터는 any로 기본값이 됩니다.
extends 절에서
예를 들어 React.Component는 Props와 State라는 두 타입 파라미터로 정의됩니다. .js 파일에서는 extends 절에 이들을 지정할 합법적인 방법이 없어요. 기본적으로 타입 인자는 any가 됩니다:
import { Component } from "react";
class MyComponent extends Component {
render() {
this.props.b; // Allowed, since this.props is of type any
}
}
타입을 명시적으로 지정하려면 JSDoc @augments를 사용하세요. 예를 들어:
import { Component } from "react";
/**
* @augments {Component<{a: number}, State>}
*/
class MyComponent extends Component {
render() {
this.props.b; // Error: b does not exist on {a:number}
}
}
JSDoc 참조에서
JSDoc의 지정되지 않은 타입 인자는 any로 기본값이 됩니다:
/** @type{Array} */
var x = [];
x.push(1); // OK
x.push("string"); // OK, x is of type Array<any>
/** @type{Array.<number>} */
var y = [];
y.push(1); // OK
y.push("string"); // Error, string is not assignable to number
함수 호출에서
제네릭 함수 호출은 인자를 사용해 타입 파라미터를 추론합니다. 때로는 주로 추론 소스 부족 때문에 어떤 타입도 추론하지 못할 수 있는데, 이런 경우 타입 파라미터가 any로 기본값이 됩니다. 예를 들어:
var p = new Promise((resolve, reject) => {
reject();
});
p; // Promise<any>;
JSDoc에서 쓸 수 있는 모든 기능을 배우려면 참조 문서를 확인해 보세요.