작성자와 사용자의 관점으로 코드 바라보기
타입시스템
- 두가지 형태
- 컴파일러에게 사용하는 타입을 명시적으로 지정하는 시스템
- 컴파일러가 자동으로 타입을 추론하는 시스템
- 타입스크립트의 타입 시스템
- 타입을 명시적으로 지정할 수 있다.
- 타입을 명시적으로 지정하지 않으면, 타입스크립트 컴파일러가 자동으로 타입을 추론한다. 타입스크립트의 타입 추론이라고 부른다.
-
함수 사용법에 대한 오해를 야기하는 자바스크립트
타입은 해당 변수가 할 수 있는 일을 결정한다. 따라서 a가 할 수 있는 일은 a의 타입이 결정한다.
해당 함수의 작성자는 매개변수 a가 number 타입이라는 가정으로 함수를 작성했다. 하지만 사용자는 사용법을 숙지하지 않은 채, 문자열을 사용하여 함수를 실행했다. 그래서 의도하지 않은 NaN 결과가 나오게 되었다.
function f2(a) {
return a * 38;
}
console.log(f2(10)); // 380
console.log(f2('Mark')); // NaN
-
타입스크립트의 추론에 의지하는 경우
타입스크립트는 타입을 지정하지 않으면 추론을 한다. a의 타입을 명시적으로 지정하지 않았기에 a는 any로 추론된다. 그래서 a 인자는 어떤 형태이던지 다 사용이 되는 형태로 볼 수 있다.
function f3(a) {
return a * 38;
}
console.log(f3(10)); // 380
console.log(f3('Mark') + 5); // NaN
사용자 입장에서 a가 any이기 때문에, 사용법에 맞게 문자열을 사용하여 함수를 실행했다. 해당 결과로 NaN(NaN도 number의 하나)이 나왔는데, 만약 해당 결과가 작성자가 의도한 것이 아니라면 해당 함수는 올바르지 않은 함수이다.
따라서 해당 함수의 작성자는 함수의 올바른 사용법을 사용자에게 전달하지 못한 것이다.
해당 함수에서 a가 타입을 지정하지 않았기에 any로 추론이 되면서 위험한 요소가 되어버린다. 위험한 요소를 없애기 위해서 컴파일러에게 이러한 형태를 작성자가 사용할 수 없도록 하는 방식이 있다. 해당 방식이 nolmplicitAny.
-
nolmplicitAny
타입을 명시적으로 지정하지 않은 경우, 타입스크립트가 추론 중 any라고 판단하게 되면, 컴파일 에러를 발생시켜 명시적으로 지정하도록 유도한다.
-
number 타입으로 추론된 리턴 타입
매개변수 a의 타입을 number라고 명시적으로 지정했으며, 명시적으로 지정하지 않은 함수의 리턴 타입은 number로 추론된다.
function f4(a: number) {
if (a > 0) {
return a * 38;
}
}
console.log(f4(5)); // 190
console.log(f4(-5) + 5); // NaN
a의 타입을 number로 지정했기 때문에 인수에 숫자가 아닌 다른 타입을 넣게 된다면 error가 발생할 것이다. 그래서 다른 형태를 넣었을 때는 해당 함수를 사용할 수 없다.
런타임상에서 a의 인자가 숫자형이면서 양수의 값일때 a*38이 되고, 아닐 경우에는 리턴되지 않기 때문에 undefined형태로 리턴될 것을 알 수 있다.
console.log(f4(-5) + 5);
해당 코드에서는 음수값 -5를 인수로 넣었기 때문에 undefined가 리턴되고 + 5 (숫자)가 더해지면서 숫자가 아닌 의미로 NaN(타입 number)이 출력된다. 해당 결과는 의도한 것이 아니기 때문에 런타임 상에서 오류가 발생하게 된다.
추가) undefined는 number가 아닌데 왜 둘이 더해서 number(NaN)이 나온 것일까?
타입스크립트가 undefined를 number로 퉁쳐버렸다.
해당 결과로 인해 타입스크립트에서 number에 undefined가 포함되어 있다는 사실을 알 수 있다.
-
strictNullChecks
모든 타입에 자동으로 포함되어 있는 null과 undefined를 제거해준다.
해당 옵션을 키지 않으면 위 함수와 같이 number에도 null, undefined를 넣을 수 있게 된다..!
// strictNullChecks
function f4(a: number) {
if (a > 0) {
return a * 38;
}
}
console.log(f4(5)); // 190
console.log(f4(-5) + 5);
// error TS2532: object is possibly 'undefined'.
매개변수의 타입을 number로 지정, 명시적으로 지정하지 않은 함수의 리턴 타입은 number 혹은 undefined로 추론된다.
console.log(f4(-5) + 5);
해당 코드에서 f4(-5)의 값은 undefined이면서 숫자 5와 더할 수 없게 된다. 옵션을 킴으로 undefined가 number로 퉁칠 수 없게 되었다.
그런데, f4(5)의 값이 190 혹은 undefined가 되어버렸다. f4를 실행하면 무조건 number 혹은 undefined로 판단하는 것이다. 이것이 문제는 아니다.
컴파일 타입에서는 number | undefined로 판단되는 것이 맞고 런타임 상에서 해당 결과물이 undefined인 경우에는 error를 throw하고 number일때만 연산을 하도록 조정을 해야 한다.
-
명시적으로 리턴 타입을 지정해야 할까?
명시적으로 지정해주는 것을 추천한다.
작성자 입장에서 코드를 작성할 때 number를 인자로 받고 리턴할 걸 계획을 하고 코드를 작성하면 지정해둔 리턴 타입과 실제 결과가 일치하는지를 타입스크립트가 확인해주기 때문에 검토하면서 작업할 수 있어서 좋다.
// error
function f5(a: number): number {
if (a > 0) {
return a * 38;
}
}
해당 코드는 error가 발생한다. if 범위 내에 있지 않은 부분은 리턴되지 않기 때문에 작업이 덜 되었다고 알려준다.
-
nolmplicitReturns
nolmplicitReturns 옵션을 키면 함수 내에서 모든 코드가 값을 리턴하지 않으면, 컴파일 에러를 발생시킨다.
// error: Not all code paths return a value.
function f5(a: number){
if (a > 0) {
return a * 38;
}
}
해당 옵션은 리턴 타입을 지정했는지 안 했는지는 중요하지 않다.
내부 코드가 모든 경우에 리턴을 하는지 안 하는지가 중요하다. 해당 코드는 if인 경우에만 리턴을 하고 if 범위 밖의 경우에는 리턴을 하지 않기 때문에 error가 발생한다.
-
매개변수에 object가 들어오는 경우
a는 object로 매개변수에 object가 들어오는 경우에는 어떤 object가 들어오는지 자바스크립트에는 명시되어 있지 않다. 그래서 다양한 object 형태가 들어오면서 의도하지 않은 결과가 발생하기도 한다.
function f6(a) {
return `이름은 ${a.name} 이고, 연령대는
${Math.floor(a.age / 10) * 10}대 입니다.`;
}
console.log(f6({ name: 'Mark', age: 38}));
// 이름은 Mark 이고, 연령대는 30대 입니다.
console.log(f6('Mark'));
// 이름은 undefined 이고, 연령대는 NaN대 입니다.
-
object literal type
object literal type으로 a의 타입을 지정해줄 수 있다.
작성자가 의도한 바와 다르게 작성하여 사용할 경우에 error를 발생시켜 사용자에게 알려준다.
function f7(a: { name: string; age: number }): string {
return `이름은 ${a.name} 이고, 연령대는
${Math.floor(a.age / 10) * 10}대 입니다.`;
}
console.log(f6({ name: 'Mark', age: 38}));
// 이름은 Mark 이고, 연령대는 30대 입니다.
console.log(f6('Mark'));
// error: Argument of type 'string' is not assignable to parameter of type '{ name: string; age: number }'.
그런데, object litetal type을 매번 길게 작성하기에 불편하니 특정한 타입으로 뽑아서 타이핑을 만들어둔다.
-
나만의 타입 만들기
interface, type, class을 사용해서 이름을 지정하여 사용할 수 있다.
interface PersonInterface {
name: string;
age: number;
}
type PersonTypeAlias = {
name: string;
age: number;
}
function f8(a: PersonInterface): string {
return `이름은 ${a.name} 이고, 연령대는
${Math.floor(a.age / 10) * 10}대 입니다.`;
}
console.log(f6({ name: 'Mark', age: 38}));
// 이름은 Mark 이고, 연령대는 30대 입니다.
console.log(f6('Mark'));
// error: Argument of type 'string' is not assignable to parameter of type 'PersonInterface'.
any
any는 어떤 것이든 할 수 있다라는 의미이다.
어떤 타입이어도 상관이 없는 타입이지만, 최대한 사용하지 않는 것이 좋다.
컴파일 타임에 타입 체크가 정상적으로 이루어지지 않기 때문이다. 그래서 컴파일 옵션 중에는 any를 써야하는데 쓰지 않으면 오류를 뱉는 옵션도 있다.(nolmplicitAny)
function returnAny(message: any): any {
console.log(message);
}
const any1 = returnAny("리턴은 아무거나");
any1.toString();
returnAny의 인자값은 어떤 값이든 들어올 수 있다. any이기 때문이다.
any1은 any이기 때문에 문자열만 사용할 수 있는 것도 사용이 가능하다.
-
any의 전파
any는 개체를 통해 계속 전파한다. 이로인해 타입의 안정성을 잃게 된다.
타입 안정성은 타입스크립트를 사용하는 주요 동기 중 하나이다. 그렇기에 필요하지 않은 경우에는 any를 사용하지 않도록 해야한다.
let looselyTyped: any = {};
let d = looselyTyped.a.b.c.d;
looselyTyped.a.b.c.d;
를 해도 Error가 나지 않는다. 그리고 이 결과는 d에 any로 들어가게 된다. 해당 any들이 개체를 통해서 전파된다는 것이다.
- any 전파 문제점
function leakingAny(obj: any) {
const a = obj.num;
const b = a + 1;
return b;
}
const c = leakingAny({ num: 0})
a도 any, b도 any, c또한 any이다. c는 number로 규정되어 나와야 하는데 말이다. 이것이 any의 전파로 인한 문제이다.
- 누수 막기
function leakingAny(obj: any) {
const a: number = obj.num;
const b = a + 1;
return b;
}
const c = leakingAny({ num: 0})
해당 문제점을 해결하고 싶다면 a에 number를 추가해준다. 이로인해 누수가 막히면서 b도 number, c도 number가 된다.
unknown
any가 가지고 있는 타입의 불안정한 요소를 해소시켜준다.
any와 짝꿍이며 any보다 타입 안정성이 있다.
unknown은 any처럼 아무거나 할당할 수 있다.
그러나, 컴파일러가 타입을 추론할 수 있게끔 타입의 유형을 좁히거나, 타입을 정해주지 않으면 다른 곳에 할당할 수 없고, 사용할 수 없다.
우리는 응용 프로그램을 작성할 때 모르는 변수의 타입을 묘사해야 할 수도 있다. 보통 모르는 변수의 타입을 any로 묘사했었다.
이러한 값은 API를 얻어서 오는 동적 콘텐츠 같은 것이다. 이러한 경우, 컴파일러와 다른 사람이 해당 코드를 읽을 때 해당 변수가 동적이기 때문에 어떤 것이든 될 수 있다는 것을 알려주는 타입이 필요했다. 기존에는 any로 사용했지만 any 이후에 오는 코드들이 any의 영향을 받으면서 코드 안정성을 해치는 문제가 생긴다.
이러한 부분을 타입적으로 강제하기 위해서 unknown을 사용하게 되었다.
never
never은 보통 return에 사용된다.
never 타입은 모든 타입의 subtype이며, 모든 타입에 할당할 수 있다. 그러나 never에는 어떤 것도 할당할 수가 없다. any조차도 never에 할당할 수 없다.
never은 잘못된 타입을 넣는 실수를 막고자 할 때 사용할 수 있다.
-
throw never
throw를 하게 되면 해당 코드 밑으로는 내려오지 않게 된다. (해당 코드 다음부터는 읽지 않는다) 즉, 해당 코드에서 함수가 끝나버린다.
해당 함수는 throw하는 경우에 대해서 어떠한 형태도 return되지 않는 다는 것으로 never키워드를 사용한다.
function error(message: string): never {
throw new Error(message);
}
-
never 추론
error 함수를 사용하면서 return을 하게 되어도, never로 추론이 된다.
function fail() {
return error("failed")
}
-
함수 내용적 never
해당 함수에서는 while이 계속 true이기 때문에 멈추지 않고 계속해서 루프가 돌아간다. 그렇기 때문에 return에 대해 never를 사용할 수 있다.
function infiniteLoop(): never {
while (true) {}
}
- 타입 입력 실수를 막기
let a: string = 'hello';
if (typeOf a !== 'string') {
a; // never
}
a가 string인데 if문을 통해 a가 string이 아니라면(string - string = never) a의 타입은 never가 나오게 된다. a에는 아무것도 할 수 없게 되면서 잘못된 타입을 넣는 실수를 막아준다.
type Indexable<T> = T extends string ? T & { [index: string]: any } : never;
const ObjectIndexable = Indexable<{}>; // never
Indexable이라는 제네릭(Generic)타입을 만들어서 T가 만약에 string이면 { [index: string]: any }
를 만든다. 나머지인 경우에는 never로 지정한다.
- typeOf Guard
declare const a: string | number;
if (typeOf a !== 'string') {
a; // number
}
string + number - string = number를 나오게 해서 a의 타입으로는 number을 나오게 하는 가드가 가능하다.
void
void는 어떤 타입도 가지지 않은 빈 상태를 의미한다. 값은 없고 타입만 있어서 void라는 값은 사용할 수 가 없다.
값을 반환하지 않는 undefined를 return하는 상태에 사용한다. 그 외에는 사용할 일이 잘 없다.
return 타입이 void라는 것은 함수의 return을 가지고 무엇이든 하지 않겠다는 명시적인 표시이다.
function returnVoid(message: string) {
console.log(message);
return;
}
const r = returnVoid("리턴이 없다.");
해당 함수 return 타입의 추론은 void로 나오게 된다.
아무 값도 반환하지 않는 return을 넣게 되어도 해당 함수의 return 타입 추론은 void로 나온다.
r의 타입 또한 void이다.
string
텍스트 형식을 참조하기 위해 ‘string’ 형식을 사용한다.
let name: string = "mark";
name = 'anna';
symbol
ECMAScript 2015의 Symbol이다.
new Symbol로 사용할 수 없다.
Symbol을 함수로 사용해서 symbol 타입을 만들어 내서 사용한다.
console.log(Symbol('foo') === Symbol('foo'));
// false
같은 함수의 같은 인자를 넣어도 독립적으로 완전히 다르기 때문에 둘이 동일한지 물어보면 동일하지 않다고 말하는 것이다.
-
Symbol 사용
프리미티브 타입의 값을 담아서 사용한다.
Symbol로 인해 생성된 값은 고유하고 수정 불가능한 값으로 만들어준다.
그래서 주로 접근을 제어하는데 사용하는 경우가 많다. (문자열만 사용해서 접근할 때 사용할 수 없고 꼭, 심볼을 얻어야만 사용할 수 있도록 할때 사용하는 경우가 많다.)
const sym = Symbol()
const obj = {
[sym]: "value"
};
console.log(obj[sym]); // "value"
-
주의!
함수로 사용할 때는 대문자 심볼 Symbol
(함수 심볼로 인해 만들어진)타입으로 사용할 때는 소문자 심볼 symbol
null & undefined
-
null
null 이라는 값으로 할당 된 것을 null이라고 한다.
null은 무언가 있는데 사용할 준비가 덜 된상태를 말한다.
null 타입은 null이라는 값만 가질 수 있다.
런타임에서 typeof 연산자를 사용해보면 object라고 나온다.
let n: null = null;
console.log(n); // null
console.log(typeof n); // object
-
undefined
undefined는 값을 할당하지 않은 변수의 값이다.
undefined는 무언가가 아예 준비가 안된 상태이다.
object의 property가 없을 때에도 undefined이다.
typeof 연산자를 사용하면 undefined라고 나온다.
let u: undefined = undefined;
console.log(u); // undefined
console.log(typeof u); // undefined
-
null과 undefined
각각 unll, undefined라는 타입을 가지며 void같이 그 자체로는 유용하지 않다.
null과 undefined는 타입과 값 모두 소문자로 작성해야 한다.
let u: undefined = undefined;
let n: null = null;
// 해당 변수에 할당할 수 있는 것들이 거의 없다.
-
설정을 통해 유용하게 사용하기
null과 undefined는 다른 모든 타입의 서브타입으로 존재한다.
설정을 통해 number에 null 또는 undefined를 할당할 수 있게 된다.
그런데, 컴파일 옵션에서 --strictNullChecks
사용하면, 다른 모든 타입의 서브타입으로 사용할 수 없게 된다. 즉, null과 undefined는 void나 자기 자신들에게만 할당할 수 있게 된다.
반대로, null과 undefined를 할당할 수 있게 하려면 union type을 이용해야 한다.
object
우리가 흔히 아는 object와 typescript의 object는 약간 다르다.
typescript의 object는 primitive type이 아닌 것이다. 즉, primitive 타입을 사용할 수 없는 곳에는 object를 사용한다고 생각하면 된다.
(primitive type ⇒ number, string, boolean, bigint, symbol, null, or undefined)
단, null은 object로 사용 가능 (null 설명 참고)
let obj: object = {};
obj = {name: 'Mark'};
obj = [{name: 'Mark'}];
obj = 39; // Error
obj = Mark; // Error
obj = true; // Error
obj = 100n; // Error
obj = Symbol(); // Error
obj = null; // Error
obj = undefined; // Error
declare function create(o: object | null): void;
// primitive type을 넣으면 Error을 뱉어낸다.
create({ prop: 0});
create(null);
create(42); // Error
crate("string"); // Error
crate(false); // Error
crate(undefined); // Error
// Object.create
Object.create(o); // Error
- 오브젝트 리터럴 (object literal)
const person1 = {name: 'Mark', age: 39};
// person1 is not "object" type.
// person1 is "{name: 'Mark', age: 39}" type.
- 전역개체 Object
const person2 = Object.create({name: 'Mark', age: 39});
const person3 = Object.create(39) // 사용불가
Object라는 전역개체를 통해 .create를 하면 안에 들어있는 오브젝트 리터럴을 통해 객체를 만들어 낸다.
Object.create 함수 안에 object type의 형태를 넣어야 한다. 즉, primitive type이 아닌 것을 넣어야 한다.
따라서 해당 함수의 인자에는 object 타입이거나 null을 넣을 수 있다.
Array
object의 일종이며, 같은 타입의 자료형을 모아둔 것을 의미한다.
주의할 점은, 배열 안의 요소들은 동일한 타입이어야 한다. 하나의 타입으로 묶을 수 없다면 array가 아닌 것이다.
- array를 표현하는 두가지 방법
// 선호하는 방법
let list: number[] = [1, 2, 3];
let list: Array<number> = [1, 2, 3];
-
다른 타입의 요소들 사용하기
또는 ( | )을 사용하여 타입이 다른 요소들을 하나로 묶는다.
따라서, 각 요소들은 number이거나, string이다.
해당 방법은 요소들이 무슨 타입인지 정확히 모를때에도 사용할 수 있다.
let list: (number | string)[] = [1, 2, 3, "4"];
-
타입을 정확히 알고 있을 때 (tuple)
첫번째 타입이 무엇이고 두번째 타입이 무엇인지 정확히 알고 있을 때가 있다. 그럴때는 array가 아닌, tuple을 사용한다.
Tuple
앞, 뒤로 다른 타입을 넣고 싶을 때, 그런데 해당 타입들이 오는 순서를 알고 있을 때 사용한다. 단, 항상 타입과 순서, 길이가 맞아야 한다.
let x: [string, number];
x = ['hello', 39];
x = [20, 30]; // Error, 타입 안 맞음
x = [10, 'anna']; // Error, 순서 안 맞음
x[2] = 'world'; // Error, 길이 안 맞음
- 구조분해할당(destructuring)을 해도 해당 타입이 정해져 있다.
const person: [string, number] = ["mark", 39];
const [first, second] = person;
// first = string, seconed = number
const [first, second, third] = person; // Error, 길이 안 맞음
TypeScript Types vs JavaScript Types
Primitive Types
타입스크립트의 기본 타입중에서 자바스크립트에서 온 타입이다.
-
해당 타입은 object와 reference 형태가 아닌 실제 값을 저장하는 자료형이다.
(즉, object와 reference는 실제 값을 저장하지 않는 것을 의미한다.)
- primitive 형의 내장 함수를 사용 가능한 것은 자바스크립트 처리 방식 덕분이다.
let name = 'mark';
name.toString();
-
ES2015기준 primitive type 6개는 boolean, number, string, symbol, null, undefined이다.
- primitive type은 literal 값으로 Primitive 타입의 서브 타입을 나타낼 수 있다.
true;
'hello';
3.14;
null;
undefined;
-
래퍼 객체로 만들 수 있다.
boolean을 대문자로 시작하는 Boolean으로 new를 통해 새로 생성해서 object를 만들 수 있다.
따라서, 이것들은 primitive type이 아닌 object이다. 해당 방법으로의 사용은 절대 권장하지 않는다! 타입으로는 사용 X
new Boolean(false);
// typeof new Boolean(false) : 'object'
new String('word');
// typeof new String('word') : 'object'
new Nember(42);
// typeof new Number(42) : 'object'
Boolean
Number
숫자를 나타내는 가장 기본적인 타입이다.
TypeScript란 무엇인가
Typed JavaScript at any Scale!
우리가 사용하는 자바스크립트에 타입이라는 개념을 적용시킨 것이다.
타입을 추가하여 자바스크립트를 확장시킨다.
자바스크립트를 이해함으로, 코드를 실행시키기 전에 에러 수정을 하는 시간을 절약시켜준다.
어떤 브라우저건 OS, 자바스크립트 실행 환경이던 사용 가능한 오픈소스이다.
타입스크립트는 프로그래밍 언어이며 컴파일 언어이다. (자바스크립트는 인터프리터 언어)
단, 전통적인 컴파일 언어와는 다른점이 많은 편이다. 그래서 트랜스파일(Transpile)이라는 용어를 사용하기도 한다.
타입스크립트는 Editor를 사용하여 작성한다.
작성된 코드는 우리가 사용하고 있는 Node나 브라우저 환경에서 바로 읽어서 사용할 수 없다. 따라서 바로 사용할 수 있는 플레인 자바스크립트로 변경해서 사용해 주어야 하는데, 이때 타입스크립트 컴파일러(프로그램)로 변경을 시켜준다.
TypeScript 설치 및 사용
-
node.js 설치
nvm 설치
-
browser 설치
그냥 크롬으로…
-
TypeScript 컴파일러 설치
First Type Annotation
Type Annotation은 타입스크립트가 가진 고유의 기능이다. 타입이라는 요소가 코드상에 들어나는 방식이다.
- typescript와 let
- let에 값 할당
// test.ts
let a = 'Mark'
a = 39 // Type 'number' is not assignable to type 'string'
39라는 값은 (넘버형 값) a에 넣을 수가 없다. a라는 곳에는 현재 무조건 string 값만 넣을 수 있기 때문이다. (a = mark로 인해)
ts에서의 let은 사용하면 a라는 변수에 특정한 값을 할당한다. a값에 할당한 데이터 타입이 지정되면서 해당 변수에는 지정된 데이터 타입만이 사용이 가능하게 된다.
- let 선언
a 변수에는 any라는 타입이 지정된다.
let a: string;
a = "Mark";
a의 타입으로 string을 지정했기 때문에 a 값에 Mark가 들어갈 수 있다. number은 불가.
a의 타입으로 numer 지정했기 때문에 39가 들어갈 수 있으며, Mark는 들어갈 수 없다.
- 함수의 인자로 타입 설정
function hello(b: number)
hello('Mark')
타입을 number로 설정했기 때문에 문자열인 Mark는 들어갈 수 없다.