TIL 23. 12. 15

타입스크립트의 기본 타입

img

원시 타입

원시 타입은 하나의 값만 저장하는 타입으로 불변 값(변경할 수 없는 값)을 정의한다. 값을 교체할 수는 있지만, 직접 변형할 수는 없다.

var bar = "baz";
console.log(bar); // baz
bar.toUpperCase();
console.log(bar); // baz

number

숫자를 의미하는 모든 값을 포함하는 타입. 정수 뿐만 아니라 소수, 음수, 특별한 숫자들도 표현할 수 있다.

  • Infinity: 양의 무한대
  • -Infinity: 음의 무한대
  • NaN: 산술 연산 불가 (not-a-number)
let num1: number = 123;
let num2: number = -123;
let num3: number = 0.123;
let num4: number = -0.123;
let num5: number = Infinity;
let num6: number = -Infinity;
let num7: number = NaN;

string

텍스트 데이터를 나타낼 때 사용하는 타입. 작은따옴표(‘’), 큰따옴표(“”) 또는 백틱(``)으로 텍스트를 감싸서 표기한 문자열 뿐만 아니라 템플릿 리터럴로 만든 문자열도 포함한다.

let str1: string = "hello";
let str2: string = 'hello';
let str3: string = `hello`;
let str4: string = `hello ${str1}`; // 템플릿 리터럴

boolean

논리 요소를 나타내며 참과 거짓 만을 저장하는 타입. truefalse 두 가지 값만 가질 수 있다.

let bool1 : boolean = true;
let bool2 : boolean = false;

null

어떤 값이 의도적으로 비어있음을 표현하는 타입. 불리언 연산에서는 거짓으로 취급한다.

let null1: null = null;
(+) 엄격한 Null 검사 옵션 - strictNullChecks

아직 값이 정해지지 않은 상태에서는 변수에 null을 할당하고 싶은 경우가 있다. 그러나 다른 타입의 변수에 null을 할당하면 오류가 발생한다.

let numA: number = null;  // ❌

null 값을 변수의 임시값으로 활용하고 싶을 때 tsconfig.json의 strcitNullChecks 옵션을 false로 설정하면 된다.

strictNullChecks: 타입스크립트에서 null 값을 null 타입 이외의 타입의 변수에 할당하는 것을 허용할지 결정하는 옵션

{
  "compilerOptions": {
    ...
    "strictNullChecks": false,
		...
  },
  "include": ["src"]
}

undefined

값이 초기화되지 않은 undefined만 포함하는 타입. 값을 할당하지 않은 변수는 undefined 값을 가진다.

let unde1: undefined = undefined;

(+) Null vs Undefined

symbol

고유한 참조값을 생성하는데 사용할 수 있는 타입. symbol은 외부에 노출되지 않고 다른 값과 중복되지 않는 유일한 값이다. 다른 원시 값은 리터럴을 통해 생성하지만 symbol은 Symbol 함수를 호출해 생성한다.

let sym1: symbol = Symbol();

리터럴

하나의 값만 포함하도록 값 자체로 만들어진 타입. 리터럴을 사용하면 다양한 종류의 값을 생성할 수 있다. 변수의 타입을 값 자체로 정의하면 정의한 값 외에는 다른 값을 사용할 수 없다.

let numA: 10 = 10;
let strA: "hello" = "hello";
let boolA: true = true;
let boolB: false = false;

배열

배열을 저장하는 변수의 이름 뒤에 타입 주석의 시작을 의미하는 콜론(:)을 작성한 다음 배열요소타입[] 형식으로 배열 타입을 정의한다.

let numArr: number[] = [1, 2, 3]
let strArr: string[] = ["hello", "world"];

Array<배열요소타입> 형태로도 배열의 타입을 정의할 수 있다. 꺽쇠와 함께 타입을 작성하는 문법을 타입스크립트에서는 ‘제네릭’ 이라고 부른다.

let boolArr: Array<boolean> = [true, false, true];

배열의 타입을 정의하는 두 형식 모두 모양만 다를 뿐 기능은 동일하기에 타이핑하기 쉬운 첫 번째 방식을 주로 사용한다.

배열에 들어가는 요소들의 타입이 다양할 경우에는 소괄호와 바( ) 를 이용해 배열 요소가 둘 중 하나의 타입에 해당하도록 타입을 정의한다.

Tip) 타입스크립트는 점진적 타입 시스템을 사용하므로 초기화한 값을 기준으로 타입을 자동으로 추론한다. 에디터에서 변수에 마우스를 올려보면 타입을 알려준다.

// 배열 요소의 타입이 string이거나 number
let multiArr: (number | string)[] = [1, "hello"]; 

바(|)를 이용해 여러 타입중 하나를 만족하는 타입을 정의하는 문법을 유니온(Union) 타입 이라고 부른다.

튜플

튜플은 자바스크립트에는 없는 타입스크립트의 특수한 타입으로 길이와 타입이 고정된 배열을 의미한다. 타입스크립트의 배열은 배열에 들어가는 타입은 고정시킬 수 있지만 길이까지 고정할 수는 없었다.

let tup1: [number, number] = [1, 2];
let tup2: [number, string, boolean] = [1, "hello", true];

튜플은 별도로 존재하는 자료형이 아닌 자바스크립트 배열이다. 타입스크립트 코드를 컴파일 해 보면 자바스크립트 배열로 변환된다. 그러므로 배열 메서드를 이용해 튜플을 다룰 수 있다.

튜플을 배열 메서드를 이용해 요소를 추가하거나 삭제 등의 연산을 할 때에는 주의해야 한다. 자바스크립트 배열로 취급하기 때문에 배열 메서드를 사용해도 오류를 출력하지 않기 때문이다.

let tup1: [number, number] = [1, 2];
tup1.push(1);
tup1.pop();
tup1.pop();

튜플의 사용 예

회원 정보를 2차원 배열로 저장하는 상황을 가정해 보자.

// 회원의 이름과 아이디를 저장해두는 배열
const users = [
  ["Lee", 1],
  ["Park", 2],
  ["Kim", 3],
];

그런데 요소의 순서가 헷갈려 반대로 저장하자 오류가 발생한다.

const users = [
  ["Lee", 1],
  ["Park", 2],
  ["Kim", 3],
  [4, "Choi"] // 추가
];

function makeUpperCase(inputString) { return inputString.toUpperCase(); }
makeUpperCase(users[3][0]) // ❌ Error
]

튜플을 사용하면 이러한 문제를 쉽게 해결할 수 있다. 배열을 사용할 때 인덱스에 따라 넣어야 할 값이 정해져 있고 순서가 중요할 때 튜플을 사용하여 실수를 방지할 수 있다.

const users: [string, number] = [
  ["Lee", 1],
  ["Park", 2],
  ["Kim", 3],
  [4, "Choi"] // ❌ Error
];

객체

원시 타입을 제외하고 가장 많이 마주치는 타입이다. 타입스크립트에서는 2가지 방식으로 객체의 타입을 정의한다.

자바스크립트의 객체는 키(key)과 값(value)으로 구성된 프로퍼티(Property)들의 집합이다.

object로 정의

let user: object = {
  id: 1,
  name: 'cs',
};

그러나 타입스크립트에서 객체의 특정 프로퍼티에 접근하려고 하면 오류가 발생한다.

let user: object = {
  id: 1,
  name: 'cs',
};

user.id; // ❌ Error

그 이유는 타입스크립트의 object 타입은 값이 객체임을 표현하는 것 외에는 아무 정보도 제공하지 않는 타입이기 때문이다. 객체의 프로퍼티에 대한 정보가 없기 때문에 프로퍼티에 접근하려 하면 오류가 발생한다.

객체의 모양을 정확히 타입으로 만드려면 객체 리터럴 타입을 활용해야 한다.

객체 리터럴 타입

객체 리터럴 타입은 다음과 같이 중괄호를 열고 객체가 갖는 프로퍼티를 직접 나열한다. 객체 리터럴과 비슷한 문법으로 객체 타입을 정의한 타입을 객체 리터럴 타입이라고 부른다.

let user: {
  id: number;
  name: string;
} = {
  id: 1,
  name: "cs",
};

user.id;

객체의 타입을 정의할 때 object 보다는 객체 리터럴 타입을 사용하는 것이 좋다. 타입스크립트에서는 객체의 타입을 정의할 때 프로퍼티를 기준으로 객체의 구조를 정의하듯 타입을 정의하기 때문이다. 이런 특징을 구조적 타입 시스템이라고 부른다.

객체의 구조가 중요하며 객체가 특정 인터페이스 또는 구조를 따르면 해당 타입으로 간주된다.

(+) 명목적 타입 시스템

명목적 타입 시스템은 구조적 타입 시스템과는 대조적으로 변수나 객체의 타입을 해당 타입의 이름 또는 명칭으로 식별한다. 객체의 타입이 명시적으로 선언되어야 한다. 클래스의 이름이 타입을 나타낸다.

class Animal:
    def make_sound(self):
        pass

class Dog(Animal):
    def make_sound(self):
        print("멍멍")

def sound_maker(animal: Animal):
    animal.make_sound()

my_dog = Dog()
sound_maker(my_dog)  # "멍멍" 출력

선택적 프로퍼티(Optional Property)

객체를 다루다보면 특정 프로퍼티를 사용하지 않는 경우도 있다. 특정 프로퍼티를 상황에 따라 생략하도록 만들고 싶다면 프로퍼티의 이름 뒤에 ? 붙여 선택적 프로퍼티로 만들어 준다.

let user: {
  id?: number;
  name: string;
} = {
  id: 1,
  name: "cs",
};

user = {
  name: "jh",
};

읽기전용 프로퍼티(Readonly Property)

특정 프로퍼티를 읽기 전용으로 만들고 싶다면 다음과 같이 프로퍼티의 이름 앞에 readonly 키워드를 붙인다. 읽기전용 프로퍼티를 사용하면 의도치 않은 프로퍼티의 수정을 방지할 수 있다.

let user: {
  id?: number;
  readonly name: string;
} = {
  id: 1,
  name: "cs",
};

user.name = "dskfd"; // ❌ Error

타입 별칭

객체 타입을 사용할 때 직접 해당 타입을 명시하였다. 그런데 같은 타입의 변수를 여러 개 사용해야 한다면 아래와 같이 타입을 중복해서 정의해서 사용하게 된다.

let user1: {
  id: number;
  name: string;
} = {
  id: 1,
  name: "cs",
}

let user2: {
  id: number;
  name: string;
} = {
  id: 2,
  name: "jh",
}

타입 정의는 중복되는 코드로, 유지보수를 어렵게 하고 가독성을 떨어트린다. 그리고 때로는 같은 타입을 재사용하거나 또 다른 이름으로 부르고 싶은 경우도 존재한다. 타입 별칭을 사용하면 변수를 선언하듯 타입을 별도로 정의할 수 있다. type 타입_이름 = 타입 형태로 타입을 정의.

type User = {
  id: number;
  name: string;
}

let user: User = {
  id: 1,
  name: "cs",
}

단, 동일한 스코프에 동일한 이름의 타입 별칭을 선언하는 것은 불가능하다. 타입 별칭은 타입 관련 문법이기 때문에 컴파일 결과 사라진다.

인덱스 시그니처

인덱스 시그니쳐는 객체 타입을 유연하게 정의할 수 있도록 돕는 특수한 문법이다.

type CountryCodes = {
  Korea: string;
  UnitedState: string;
  UnitedKingdom: string;
};

// 다양한 국가들의 영어 코드를 저장하는 객체
let countryCodes: CountryCodes = {
  Korea: "ko",
  UnitedState: "us",
  UnitedKingdom: "uk",
};

countryCodes에 100개의 프로퍼티가 추가되어야 한다면 타입 정의에도 각 프로퍼티를 모두 정의해주어야 한다. 그런데 아직 추가되어야 할 프로퍼티 이름과 값을 알지 못한다면?

객체의 키와 값의 타입 규칙을 알고 있다면 ‘인덱스 시그니쳐’를 이용하여 간단하게 타입을 정의할 수 있다.

type CountryCodes = {
  [key: string]: string;
};

let countryCodes: CountryCodes = {
  Korea: "ko",
  UnitedState: "us",
  UnitedKingdom: "uk",
  // (... 약 100개의 국가)
  Brazil : 'bz'
};

주의할 점은 인덱스 시그니쳐는 정의된 규칙을 위반하지만 않으면 모든 객체를 허용한다. 빈 객체는 이를 위반하지는 않으므로 오류가 발생하지 않는다. 반드시 포함해야 하는 프로퍼티가 있다면 직접 명시한다. 이때 직접 추가한 프로퍼티 값의 타입은 인덱스 시그니쳐로 정의한 값의 타입과 호환되거나 일치해야 한다.

type CountryCodes = {
  [key: string]: string;
  Korea: string;
};

enum 타입

여러가지 값들에 각각 이름을 부여해 열거해두고 사용하는 타입. 열거형 타입은 자바스크립트에는 존재하지 않고 타입스크립트에서만 사용할 수 있다. enum 멤버에 숫자 값을 직접 할당하지 않아도 0부터 1씩 늘어나는 값으로 자동 할당된다.

enum Role {
  ADMIN = 0, 
  USER = 1,
  GUEST = 2,
}

enum Role2 {
  ADMIN, // 0
  USER,  // 1
  GUEST, // 2
}

const user1 = {
  name: "admin",
  role: Role.ADMIN,
}

자동 할당되는 값은 기본적으로 0부터 시작하고 이 값을 변경하고 싶다면 시작하는 위치에 값을 직접 할당해주면 된다. 그 아래의 멤버들은 1씩 증가된 값으로 자동으로 할당된다.

enum Role {
  ADMIN = 10, // 10 
  USER,  	  // 11 
  GUEST, 	  // 12
}

enum의 멤버에는 숫자 말고도 문자열 값도 할당할 수 있다. 숫자만으로는 그 값이 어떤 의미인지 추측하기 힘들 때 프로퍼티의 값으로 문자열을 사용할 수 있다.

enum Language {
  korean = "ko",
  english = "en",
}

const user1 = {
  name: "admin",
  language: Language.korean,// "ko"
};

그런데, enum과 같은 기능은 객체 리터럴 타입을 사용해 구현할 수 있었다.

type Language = {
  korean = "ko",
  english = "en",
}

type 또는 interface가 있는데 굳이 enum 타입을 써야 하는 이유가 무엇일까?

enum의 컴파일 결과

타입스크립트 타입 관련 코드는 컴파일할 때 사라진다. 그런데 enum은 컴파일될 때 다른 타입들처럼 사라지지 않고 자바스크립트 객체로 변환된다. 즉, 런타임에도 객체로 존재해 값으로 사용할 수 있다.

(function (Language) {
    Language["korean"] = "ko";
    Language["english"] = "en";
})(Language || (Language = {}));

Any 타입 (최대한 사용하지 말 것)

any 타입은 특정 변수의 타입을 모를 때, 특정 값으로 인하여 타입 검사 오류가 발생하는 것을 원하지 않을 때 사용할 수 있다.

let anyVar: any = 10;
anyVar = "hello";

anyVar = true;
anyVar = {};

anyVar.toUpperCase();
anyVar.toFixed();
anyVar.a;

any 타입은 어떠한 타입 검사도 받지 않기 때문에 아무 타입의 값이나 담아 사용할 수 있고 또 다양한 타입의 메서드도 마음대로 호출해서 사용해도 문제가 되지 않는다.

그런데, 위의 코드를 실행하면 ‘런타임’에 오류가 발생한다. 타입스크립트를 쓰는 이유는 실행 전에 타입 오류를 검사해서 안정성이 높이는 것인데 any 타입은 타입 검사를 받지 않으므로 타입스크립트를 쓰는 의미가 없다.

값의 타입을 모르는 경우는 unknown 타입을 사용한다.

Unknown 타입

unknown 타입의 변수는 any 타입과 마찬가지로 어떤 타입의 값이든 다 저장할 수 있다.

let unknownVar: unknown;

unknownVar = "";
unknownVar = 1;
unknownVar = () => {};

any 타입과 차이점은 unknown 타입의 값은 어떤 타입의 변수에도 저장할 수 없다. 또한 unknown 타입의 값은 어떤 연산에도 참여할 수 없고, 어떤 메서드도 사용할 수 없다.

let num: number = 10;

let unknownVar: unknown;

num = unknownVar; // ❌ Error
unknownVar * 2 // ❌ Error

모든 값을 할당 받을 수 있게 되지만, 반대로 그 어떤 타입의 변수에도 할당할 수 없고, 모든 연산에 참가할 수 없게 된다.

unknown 타입의 값을 활용하고 싶다면 타입을 좁혀 특정 타입임을 보장할 수 있게 되면 해당 값의 타입이 자동으로 바뀐다.

if (typeof unknownVar === "number") {
  unknownVar * 2; // let unknownVar: number
}

Void

아무런 값도 없음을 의미하는 타입. 함수에 return문이 없거나 명시적으로 값을 반환하지 않을 때 추론된다.

function func1(): void {
  console.log("hello");
}

function func2() {
  return;
}

아무것도 반환하지 않는 함수의 반환 값 타입을 null이나 undefined을 사용하면 되지 않나? 하지만 null이나 undefined으로 반환 값 타입을 정의하면 실제로 null 혹은 undefined으로 값을 반환해야 한다.

🚨 TypeScript 5.1로 업데이트 되면서 아무것도 반환하지 않는 타입으로 undefined을 명시해도 문제가 발생하지 않는다.

“그러면 void를 왜 써야하는가?”

왜 TypeScript는 void 타입을 사용해도 값을 return 할 수 있을까?

콜백함수가 어떤 값을 반환하지 않아야 할 때, 위에서 본 것처럼 undefined을 사용할 수 없다. 실제 undefined을 반환해야 하기 때문에. 그렇다면 아무것도 반환하지 않는 자바스크립트의 forEach 메서드의 경우는?

forEach(callbackfn: (value: T, index: number, array: T[]) => void, thisArg?: any): void;

forEach 콜백함수에서 때로는 반환 값이 생긴다. e.g. arr.push(element)의 반환 값은 배열의 길이다. 그런데 forEach 콜백함수의 반환 값 타입이 number 임에도 오류가 나지 않는다.

void는 return에 어떤 값이 와도 상관은 없지만, 사용하지 않는다는 뜻으로 해석한다. 그 이유는 자바스크립트의 기존 동작을 해치지 않고 타입을 보장하기 위함이다.

void 반환 타입으로의 문맥적 타이핑은 함수를 아무것도 반환하지 않도록 강제하지 않습니다. 이를 설명하는 또 다른 방법은, void 반환 타입을 가지는 문맥적 함수 타입(type vf = () => void)가 구현되었을 때, 아무 값이나 반환될 수 있지만, 무시됩니다. 공식 문서

void를 반환 타입으로 지정했을 때, undefined 외에 다른 타입의 값도 반환 할 수도 있다. 하지만 반환 타입이 void로 명시되어 있기 때문에 다른 값을 반환한다 하더라도 타임 검사에서 이를 체크해주기 때문에 안전한 타이핑이 가능하다.

type VoidFunc = () => void;
const myFunc: VoidFunc = () => {
  return 'hello';
};

let myString = myFunc(); // myString: void
myString = 'something';  // ❌ Error

게다가 ‘반환 값이 존재하지 않아야 한다’라는 의미로도 void를 사용한다.

Never

함수가 어떠한 값도 반환할 수 없는 상황에 해당 함수의 반환값 타입을 정의할 때 사용. 무한 루프와 같이 뭔가를 반환한다는 것 자체가 ‘불가능’하거나 의도적으로 오류를 발생 시키는 함수에서 사용한다.

// 반환 자체가 불가능
function funcA(): never {
  while (true) {}
}

// 실행과 동시에 중지됨
function funcB(): never {
  throw new Error();
}

void를 쓸 수 있지 않나? 하지만 함수가 정상적으로 종료되어 반환 값이 없어 void를 반환하는 것과 반환 자체가 불가능한 함수의 반환 값으로 void를 사용하는 것은 꽤 다르다.

이처럼 불가능한 값의 타입을 정의할 때 never 타입을 사용한다. 변수의 타입을 never로 정의하면 any를 포함해 그 어떠한 타입의 값도 이 변수에 담을 수 없다.

카테고리:

업데이트:

댓글남기기