Js types vs. Ts types
JS에도 다음과 같이 타입의 개념은 존재한다.
// js에도 타입의 개념은 존재한다
function add(x, y) {
if (typeof x !== 'number' || typeof y !== 'number') throw new Error('type is not a number');
return x + y;
}
// ts에서는 다음과 같이 작성할 수 있다
function add2(x: number, y: number) {
return x + y;
}
- 자바스크립트는 데이터의 타입을 동적으로 결정하는 언어이다(dynamic typing).
- typeof 연산자를 이용하면 스크립트가 실행될 때 데이터의 타입을 제한할 수 있다.
- 어플리케이션이 실행됨에따라 에러를 발생시킨다.
- 반면 타입스크립트는 데이터의 타입을 정적으로 결정하는 언어이다(static typing).
- 개발 도중에 데이터의 타입을 정의할 수 있다.
- 즉 정의한 타입과 다른 타입의 데이터를 할당하면, 개발 도중에 에러가 발생한다.
- +) ts는 js보다 더 많은 타입을 제공한다(ex. 사용자가 직접 타입을 정의할 수 있다).
- 즉, js는 스크립트를 실행할 때(런타임), ts는 js로 변환될 때(컴파일 시) 타입이 결정된다.
Type Inference
타입스크립트가 자동으로 타입을 추론할 수 있기 때문에, 모든 변수에 타입을 명시해야 하는 것은 아니다.
// 자동으로 age의 type이 number로 추론된다.
let age = 13;
// 따라서 string을 재할당하려고 하면 에러가 발생한다.
// type '13' is not assinable to type 'number'
age = '13';
Types 공식문서
1. Variable
number, string, boolean
Object
js에서 객체는 모두 같은 객체 타입이지만, ts에는 더 구체적인 형식의 객체가 존재한다. 즉 객체에도 여러 타입이 존재한다.
// 1.
const person = {
name: 'Harry',
age: 30,
}
// 타입스크립트는 위의 객체의 타입을 아래와 같이 추론한다.
/*
person {
name: string;
age: number;
}
*/
// 따라서 다음과 같이 타입에 포함되어 있지 않은 key에 접근하려고 하면 에러가 발생한다.
person.email;
// 2.
const person:object = { // const person: {}과 같다
name: 'Harry',
age: 30
}
// 타입스크립트는 위의 객체 타입을 아래와 같이 추론한다.
/*
person: object
*/
// 아래의 경우에서 에러가 발생한다!
person.name;
// 3.
const person: {
name: string;
age: number;
} = {
name: 'Harry',
age: 30
}
- 첫번째 예제에서 ts가 추론한 타입은 언뜻 js 객체처럼 보이지만 , 대신 ;을 사용한다는 점에서 차이가 있다(객체와 타입은 같지 않다).
- 타입스크립트에 의해 추론된 객체의 타입은 key와 value 형태로 type을 갖는다.
- 두번째 예제에서와 같이 객체의 타입을 object로 명시했을 때, 타입스크립트는 해당 객체에 대한 어떠한 정보도 받지 못한다.
- 세번째 예제처럼 타입을 작성할 수도 있지만, 첫번째 예제처럼 작성한 뒤 ts의 타입추론을 이용하는 것이 더욱 효율적이다.
Array
const person = {
name: 'Harry',
age: 30,
hobbies: ["guitar", "swimming", "baking"],
}
/*
person {
name: string;
age: number;
hobbies: string[];
}
*/
- hobbies에 접근하면, 배열에서 사용할 수 있는 메서드를 자동완성 처리해 준다.
- 마찬가지로, hobbies의 요소에 접근하면 string에서 사용할 수 있는 메서드를 자동완성해 준다.
- 배열이나 문자열에서 사용할 수 없는 메서드를 사용하려고 하면, 에러가 발생한다.
Tuple
tuple이란 길이와 타입이 고정된 배열이다.
// 1.
const person = {
name: 'Harry',
age: 30,
hobbies: ["guitar", "swimming", "baking"],
code: [202200000, 'student'],
}
/*
person {
name: string;
age: number;
hobbies: string[];
code : (string | number)[]; // 문자열 또는 숫자로 이루어진 배열
}
*/
person.code.push('professor'); // 에러가 발생하지 않는다
// 2.
const person : {
name: string;
age: number;
hobbies: string[];
code : [number, string]; // tuple type!
} = {
name: 'Harry',
age: 30,
hobbies: ["guitar", "swimming", "baking"],
code: [202200000, 'student'],
}
person.code[1] = 202200000; // 에러가 발생한다
person.code = [202200000, 'student', 'hello']; // 에러가 발생한다
person.code.push('professor'); // 에러가 발생하지 않는다
- 첫번째 예제에서 code의 타입이 문자열이나 숫자로 이루어진 배열로 추론되기 때문에, 다른 숫자나 문자열 데이터를 배열에 push 하는 것이 가능하다.
- 이런 상황을 방지하고 싶을 때 사용할 수 있는 것이 Tuple 타입이다. => ts는 code의 타입을 tuple로 추론하지 않았기 때문에 두번째 예제와 같이 타입을 명시해 주어야 한다.
- [ number, string ]
- 두 개의 요소만을 갖는 배열임을 의미한다.
- person.code를 길이가 다른 배열이나, 요소의 타입이 다르게 구성된 배열로 재할당하려고 하면 에러가 발생한다.
- 그러나 person.code.push를 이용해서 배열의 길이를 다르게 만드는 것은 가능하다(ts가 에러를 보여주지 못함). => 단, number나 string이 아닌 타입의 데이터를 push하는 것은 불가능하다.
- 첫번째 요소의 타입은 숫자, 두번째 요소의 타입은 문자열이어야 한다.
- 두 개의 요소만을 갖는 배열임을 의미한다.
enum | enumeration(열거)
// 1.
const ADMIN = 0;
const USER = 1;
const user = {
name: 'harry',
role: USER,
}
if(user1.role === USER) {
// do something...
}
// 2.
enum Role { ADMIN, READ_ONLY_USER, AUTHORIZED_USER };
const user = {
name: 'harry',
role: Role.ADMIN,
}
if(user.role === Role.ADMIN) {
// do something...
}
/*
user {
name: string;
role: Role;
}
*/
enum Role { ADMIN = 5, READ_ONLY_USER, AUTHORIZED_USER };
// READ_ONLY_USER, AUTHORIZED_USER에 6, 7이 할당된다
enum Role { ADMIN = 'ADMIN', READ_ONLY_USER = '1' , AUTHORIZED_USER = '100' };
// 이런 식의 할당도 가능하다
- 첫번째 예제
- ADMIN, USER와 같이 상수를 따로 관리해 주어야 한다.
- user1.role의 타입이 number로 추론된다. => user1.role = 123; 과 같이 재할당이 가능하다.
- 두번째 예제와 같이 enum을 이용하면 첫번째 예제로 작성했을 때의 문제를 해결할 수 있다.
- enum은 사용자가 직접 지정하는 커스텀 타입 중 하나이다.
- enum 키워드를 사용해서 선언하며, 첫번째 글자는 대문자로 하는 것이 관행이다.
- 중괄호 안에 사용될 값을 작성한다(모든 글자를 대문자로 하는 것은 선택사항이다).
- ts는 괄호 안에 작성된 값들에 숫자를 할당한다. => ADMIN = 0, READ_ONLY_USER=1, ...
Any
let a : any = 10;
let isBoolean : boolean = a; // 에러가 발생하지 않는다.
- 타입이 any인 데이터는 어디에든지 할당할 수 있다.
- 바닐라 js를 사용하는 것과 다름없기 때문에, ts를 사용하는 이유의 본질을 흐리는 타입이기도 하다.
- ??? 어떤 값이 런타임에 결정되어야 하는 경우가 아니라면, any를 사용하지 않는다. => 어떤 경우가 구체적으로 있는지?
Union
union 타입을 이용하면 여러 타입 중 하나를 받을 수 있다.
// combine의 매개변수 x, y는 숫자나 문자 타입의 인자를 받는다
function combine(x: number | string, y: number | string) {
const result = x + y;
return result;
}
// 하지만 위와 같이 작성하면 에러가 발생한다
// Operator '+' cannot be applied to types 'string | number' and 'string | number'.
// 다음과 같이 런타임 조건검사를 이용해서 에러를 해결할 수 있다
function add(x: number | string, y: number | string) {
if(typeof x === 'number' && typeof y === 'number' ) return x + y;
else return x.toString() + y.toString()
}
- + 연산자는 숫자와 문자열 타입의 데이터에 모두 사용할 수 있는 연산자이므로, 에러가 발생하지 않아야 할 것 같다.
- 그러나 에러가 발생한다. ts는 union 타입 내에 어떤 타입이 있는지는 분석하지 못하기 때문이다.
- number | string이라고 타입을 전달하면, ts는 해당 변수의 타입이 union 타입이라고만 인식하고, 그 타입에 무엇이 있는지는 인식하지 못한다.
- 즉, ts의 입장에서 x와 y는 숫자 또는 문자열이 아니라 union 타입이기 때문에, + 연산자를 사용할 수 없는 경우가 있다고 분석해서 에러를 보여준다.
Literal
리터럴 타입이란 string, number... 등과 같은 포괄적인 타입이 아니라, 정확한 값을 가지는 타입을 말한다. 리터럴 타입은 union 타입과 함께 유용하게 사용할 수 있다.
let x = 3;
const y = 5; // 리터럴 타입
/*
ts는 아래와 같이 타입을 추론한다
x: number
y: 5
*/
- const로 선언한 변수에 할당되는 값은 변하지 않기 때문에, ts는 y의 타입을 5라고 추론한다.
function combine(
x : number | string,
y : number | string,
returnType : "as-number" | "as-string",
) {
if(type === 'as-number') return +x + +y;
else return x.toString() + y.toString();
}
combine('hello', 'world', 'as-string');
- 위의 returnType과 같이 비교적 적은 데이터를 저장하는 경우에는, enum 대신 리터럴 타입을 고려해 볼 수 있다.
type Alias
type 키워드를 이용하면 사용자 정의 타입을 지정할 수 있는데, 여기에 union 타입을 저장할 수 있다. union 뿐만 아니라 타입을 직접 생성하는 것도 가능하다.
type Combinable = number | string;
type CombineReturnType = "as-number" | "as-text";
type User = {
name: string;
age: number;
email: string;
}
function isAdult(user: User) {
return user.age > 18;
}
any vs. unknown
any를 사용하는 것 보다는 unknown을 사용하는 것이 낫다.
// 1. unknown
let userInputUnknown: unknown;
userInputUnknown = 12;
userInputUnknown = 'hello world';
// 2. any
let userInputAny: any;
userInputAny = 12;
userInputAny = 'hello world';
// 3.
let userName: string;
userName = userInputAny ; // 에러가 발생하지 않는다 => any는 어디에든 할당할 수 있다
userName = userInputUnknown ; // 에러 발생 => unknown을 string에 할당할 수 없다
if(typeof userInputUnknown === 'string') { // 타입을 체크하면,
userName = userInputUnknown; // 에러가 발생하지 않는다
}
2. function
return type
- void vs. undefined
+) ??? 함수에서 콜백함수를 인자로 받을 때와 받지 않을 때에 따라서 void의 의미가 다른 건지? => 콜백을 받지 않는 함수에서 void는 그 함수 내부에 return 키워드가 사용되지 않았다는 것을 의미하고, 콜백을 받는 함수에서 void는 그 함수의 반환값을 변환하는 것이 불가능하다는 것을 의미한다고 이해했는데 맞는 건지 잘 모르겠다.
함수를 작성할 때, 매개변수 뿐만 아니라 반환값에 대해서도 타입을 지정할 수 있다.
function add(x: number, y: number) { // function add(x: number, y: number): number로 굳이 작성X
return x + y;
}
/*
다음과 같이 타입이 추론된다
function add (x: number, y: number) : number
*/
function combine(x: number, y: number) {
return x.toString() + y.toString();
}
/*
다음과 같이 타입이 추론된다
function add (x: number, y: number) : string
*/
- x, y의 타입은 number 이다.
- add 함수의 return 타입은 number로 추론된다.
- ts는 함수가 반환하는 값을 분석하고, 이 값의 타입을 추론하기 때문에 특별한 이유가 없다면 반환값의 타입을 굳이 명시해 주지 않아도 된다.
js에서 void 라는 타입은 존재하지 않지만, undefined은 값(value)이자 타입 중 하나로 존재한다. 예를 들어 return 문이 작성되지 않은 함수의 경우, 자동으로 undefined 값을 반환하게 된다. 물론 return undefined; 이라고 작성해도 undefined을 반환한다.
반면 ts에는 void와 undefined 두 가지 타입이 모두 존재한다. return 키워드를 사용하지 않은 함수의 반환값 타입은 void이고, 아무 것도 반환하지 않더라도 return 키워드를 사용한 함수의 반환값 타입은 undefined이다.
// 타입 에러가 발생하지 않는다
function print(text: string): undefined {
console.log(text);
return;
}
// 타입 에러가 발생하지 않는다
function print(text: string): void {
console.log(text);
}
// 타입 에러가 발생한다
function print(text: string): void {
console.log(text);
return;
}
- 함수 반환값의 타입이 void나 any가 아니라면, 반드시 return 키워드를 사용해야 한다.
- 함수 반환값의 타입이 void라는 것은, 함수 내에서 아무 것도 반환하지 않는다는 것을 의미한다. => return 키워드가 있다는 것은, 함수 반환값의 타입이 void가 아니라는 것이다.
- js에서 위의 세가지 예제 모두는 실제로 undefined을 반환하지만, ts에서 void와 undefined에는 차이가 있다.
- never
function generateError(message: string, code: number) {
throw { message, code }
}
// undefined이 출력될 것 같지만, 콘솔에는 아무것도 출력되지 않는다
console.log(typeof generateError('not found', 404));
// 다음과 같이 작성할 수 있다
function generateError(message: string, code: number):never {
throw { message, code }
}
- throw 키워드를 이용해서 에러를 전달하면, 항상 스크립트를 crash 한다.
- +) 그렇기 때문에 try - catch 문을 이용해서 스크립트가 crash 된 이후를 처리하는 로직을 작성한다.
- generateError와 같이 이런 일을 하는 함수는 절대로 값을 생성하지 않는다. => 반환 값의 never로 지정할 수 있고, 이와 같이 명시해 주는 것이 더 좋다.
- 기본적으로는 반환값을 void로 추론한다.
function type
함수 그 자체도 하나의 타입이 될 수 있다.
// test의 타입은 number 타입인 a, b 두 인자를 받아서, 숫자 타입의 값을 반환하는 함수이다.
let test: (a: number, b: number) => number;
function add(x: number, y: number) {
return x + y;
}
function print(x: number) {
console.log(x);
}
test = add;
test = prinit; // 타입 에러가 발생한다
callback
function addAndHandle(a: number, b: number, callback: (x: number) => void) {
const result = a + b;
callback(result);
}
// 1.
addAndHandle(1, 100, (result) => {console.log(result)});
// 2.
addAndHandle(1, 100, (result, result2) => {console.log(result, result2)});
// 3.
addAndHandle(1, 100, (result) => {
console.log(result);
return result;
}
);
// 4.
function addAndHandle(a: number, b: number, callback: (x: number) => void) {
const result = a + b;
callback(result);
// addHandle이 콜백의 반환값에 무언가를 실행하므로, 에러가 발생한다
callback(result).toString();
}
- 첫번째 예제는 에러 없이 잘 동작한다.
- 두번째 예제는 콜백 함수에 정의된 타입보다 더 많은 인자를 받고있기 때문에 타입 에러가 발생한다.
- 세번째 예제는 콜백 함수의 반환값 타입이 void로 정의되어 있기 때문에 타입 에러가 발생해야 할 것 같지만(result를 반환하고 있으므로), 그렇지 않다.
- 이것은 버그가 아니라, 우리가 반환값으로 void 타입을 지정했기 때문에 발생하는 일이다.
- callback에서 void를 명시하면, addAndHandle에서 callback이 반환하는 결과를 변환하는 일을 전혀 하지 않는다는 것을 의미한다.
- => 따라서 네번째 예제에서 에러가 발생한다.
'Study > TypeScript' 카테고리의 다른 글
Generic (0) | 2022.07.24 |
---|---|
Advanced Types (0) | 2022.07.23 |
Interface (0) | 2022.07.21 |
Class (0) | 2022.07.21 |
tsconfig | ts compiler (0) | 2022.07.17 |