본문 바로가기

Study/TypeScript

Types

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;
}

 

  1. 자바스크립트는 데이터의 타입을 동적으로 결정하는 언어이다(dynamic typing).
    • typeof 연산자를 이용하면 스크립트가 실행될 때 데이터의 타입을 제한할 수 있다. 
    • 어플리케이션이 실행됨에따라 에러를 발생시킨다. 
  2. 반면 타입스크립트는 데이터의 타입을 정적으로 결정하는 언어이다(static typing).
    • 개발 도중에 데이터의 타입을 정의할 수 있다. 
    • 즉 정의한 타입과 다른 타입의 데이터를 할당하면, 개발 도중에 에러가 발생한다. 
    • +) ts는 js보다 더 많은 타입을 제공한다(ex. 사용자가 직접 타입을 정의할 수 있다).
  3. 즉, 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
}

 

  1. 첫번째 예제에서 ts가 추론한 타입은 언뜻 js 객체처럼 보이지만 , 대신 ;을 사용한다는 점에서 차이가 있다(객체와 타입은 같지 않다). 
    • 타입스크립트에 의해 추론된 객체의 타입은 keyvalue 형태로 type을 갖는다. 
  2. 두번째 예제에서와 같이 객체의 타입을 object로 명시했을 때, 타입스크립트는 해당 객체에 대한 어떠한 정보도 받지 못한다. 
  3. 세번째 예제처럼 타입을 작성할 수도 있지만, 첫번째 예제처럼 작성한 뒤 ts의 타입추론을 이용하는 것이 더욱 효율적이다. 

Array 

const person = {
    name: 'Harry',
    age: 30,
    hobbies: ["guitar", "swimming", "baking"],
}
/*
person {
  name: string; 
  age: number; 
  hobbies: string[]; 
}
*/

 

  1. hobbies에 접근하면, 배열에서 사용할 수 있는 메서드를 자동완성 처리해 준다. 
  2. 마찬가지로, hobbies의 요소에 접근하면 string에서 사용할 수 있는 메서드를 자동완성해 준다. 
  3. 배열이나 문자열에서 사용할 수 없는 메서드를 사용하려고 하면, 에러가 발생한다. 

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'); // 에러가 발생하지 않는다

 

  1. 첫번째 예제에서 code의 타입이 문자열이나 숫자로 이루어진 배열로 추론되기 때문에, 다른 숫자나 문자열 데이터를 배열에 push 하는 것이 가능하다.
    • 이런 상황을 방지하고 싶을 때 사용할 수 있는 것이 Tuple 타입이다. => ts는 code의 타입을 tuple로 추론하지 않았기 때문에 두번째 예제와 같이 타입을 명시해 주어야 한다. 
  2. [ 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' }; 
// 이런 식의 할당도 가능하다

 

  1. 첫번째 예제
    • ADMIN, USER와 같이 상수를 따로 관리해 주어야 한다.
    • user1.role의 타입이 number로 추론된다. => user1.role = 123; 과 같이 재할당이 가능하다. 
  2. 두번째 예제와 같이 enum을 이용하면 첫번째 예제로 작성했을 때의 문제를 해결할 수 있다. 
    • enum은 사용자가 직접 지정하는 커스텀 타입 중 하나이다. 
    • enum 키워드를 사용해서 선언하며, 첫번째 글자는 대문자로 하는 것이 관행이다. 
    • 중괄호 안에 사용될 값을 작성한다(모든 글자를 대문자로 하는 것은 선택사항이다).
    • ts는 괄호 안에 작성된 값들에 숫자를 할당한다. => ADMIN = 0, READ_ONLY_USER=1, ... 

Any

let a : any = 10;
let isBoolean : boolean = a; // 에러가 발생하지 않는다.

 

  1. 타입이 any인 데이터는 어디에든지 할당할 수 있다. 
  2. 바닐라 js를 사용하는 것과 다름없기 때문에, ts를 사용하는 이유의 본질을 흐리는 타입이기도 하다. 
  3. ??? 어떤 값이 런타임에 결정되어야 하는 경우가 아니라면, 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()
}

 

  1. + 연산자는 숫자와 문자열 타입의 데이터에 모두 사용할 수 있는 연산자이므로, 에러가 발생하지 않아야 할 것 같다. 
  2. 그러나 에러가 발생한다. 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 
*/

 

  1. x, y의 타입은 number 이다.
  2. add 함수의 return 타입은 number로 추론된다. 
  3. 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; 
}

 

  1. 함수 반환값의 타입이 void나 any가 아니라면, 반드시 return 키워드를 사용해야 한다. 
  2. 함수 반환값의 타입이 void라는 것은, 함수 내에서 아무 것도 반환하지 않는다는 것을 의미한다. => return 키워드가 있다는 것은, 함수 반환값의 타입이 void가 아니라는 것이다.
  3. 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 }
}

 

  1. throw 키워드를 이용해서 에러를 전달하면, 항상 스크립트를 crash 한다.
    • +) 그렇기 때문에 try - catch 문을 이용해서 스크립트가 crash 된 이후를 처리하는 로직을 작성한다. 
  2. generateError와 같이 이런 일을 하는 함수는 절대로 값을 생성하지 않는다. => 반환 값의 never로 지정할 수 있고, 이와 같이 명시해 주는 것이 더 좋다. 
  3. 기본적으로는 반환값을 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(); 
}

 

  1. 첫번째 예제는 에러 없이 잘 동작한다. 
  2. 두번째 예제는 콜백 함수에 정의된 타입보다 더 많은 인자를 받고있기 때문에 타입 에러가 발생한다. 
  3. 세번째 예제는 콜백 함수의 반환값 타입이 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