Typescript 전반적으로 총 정리하기

Sheryl Yun·2023년 1월 1일
0

타입스크립트

목록 보기
8/11
post-thumbnail

개념

  • 자바스크립트에 강한 타입 시스템을 적용
  • 타입스크립트 컴파일러를 통해 최종적으로 자바스크립트로 컴파일
  • 자바스크립트의 Superset(상위 호환) 개념

사용하는 이유

  • 런타임 이전의 컴파일 단계(코드를 입력하는 동안)에서 대부분의 에러를 확인할 수 있다.

    런타임은 실제 고객이 마주할 수 있는 환경으로, 고객이 사이트를 이용하는 중 에러가 발생하는 단계라서 에러 발생 시 처리 비용이 크다.

  • 타입을 강제하는 것은 예측 가능한 코드와 디버깅이 쉽게 만든다.

  • 강력한 자동 완성 기능을 통해 생산성을 높인다.

  • 인터페이스, 제네릭 등 객체 지향적 기능을 제공하여 대규모 프로젝트에 적합하다.

기본 원리

타입스크립트의 기본 원리는 구조적 타입 시스템(Structural type system, 일명 '덕 타이핑')

  • 타입 체크 시 해당 변수가 가지고 있는 모양에 집중하는 것

예: dictionary 또는 map이라고 불리는 구조는 안에 key, value가 몇 쌍이 있던 똑같은 모양으로 본다.

특정 타입이 아니어도 특정 타입의 '모양'을 갖고 있으면 그대로 타입 체크를 통과하는 것

사용

  • .ts 또는 .tsx 라는 확장자를 갖는다.
  • 작성 후 타입스크립트 컴파일러를 통해 JS 파일로 컴파일 된다.
$ tsc sample.ts
	# compiled to `sample.js`

tsconfig.json 파일

tsconfig.json 파일로 컴파일러 옵션을 관리할 수 있다.

  • "include""exclude" 옵션: 컴파일에 포함될 경로와 제외할 경로를 설정
{
  "compilerOptions": {
    "strict": true,
    "target": "ES6",
    "lib": ["ES2015", "DOM"],
    "module": "CommonJS"
  },
  "include": [
    "src/**/*.ts"
  ],
  "exclude": [
    "node_modules"
  ]
}

기능

  • 호환성: 자바스크립트가 실행되는 모든 플랫폼에서 사용 가능
  • 객체 지향적: 클래스, 인터페이스, 모듈 등의 기능 제공
  • 정적 타입 사용: 코드를 입력하는 동안 오류 체크
  • 에러 메시지에 에러 코드 명시: 에러 코드를 검색하면 에러에 대한 정보를 쉽게 얻을 수 있음
  • 자바스크립트처럼 DOM 제어 가능
  • 최신 ECMAScript 지원: ES6 이상의 최신 자바스크립트 문법 사용 가능

TS Playground

  • 공식 문서에서 제공하는 REPL 에디터
  • 작성한 내용이 타입스크립트 컴파일러 옵션에 따라 어떻게 자바스크립트로 변환되는지 바로 확인 가능

타입 종류 (기본 타입 제외)

기본 타입 외에 다음 타입들이 있다.

  • any: 어떤 타입이든 가능
  • unknown: 알려지지 않은 타입, 타입을 지정하라고 명시
  • never: 절대 안 되는 타입
  • void: 함수에서 반환하는 값이 없거나 undefined를 반환하는 타입

튜플 (Tuple)

정해진 타입으로 이루어진 고정된 길이(length)의 배열을 표현하는 타입
자리도 정해지고 갯수도 정해진다.

let tuple: [string, number];

tuple = ['a', 1];
tuple = ['a', 1, 2]; // Error - TS2322
tuple = [1, 'a']; // Error - TS2322
  • 개별 변수를 하나의 튜플로 모아서 배열로 선언 가능
  • 인덱스로 값을 꺼낼 수 있다.
// 개별 변수
let userId: number = 1234;
let userName: string = 'HEROPY';
let isValid: boolean = true;

// 단일 튜플로 모아서 선언
let user: [number, string, boolean] = [1234, 'HEROPY', true];

console.log(user[0]); // 1234
console.log(user[1]); // 'HEROPY'
console.log(user[2]); // true
  • 다음과 같은 2차원 배열 형태의 튜플도 가능하다.
let users: [number, string, boolean][]; // 2차원 배열로 튜플 선언

users = [[1, 'Neo', true], [2, 'Evan', false], [3, 'Lewis', true]];
  • 특정 값으로 타입을 대신할 수도 있다.
let tuple: [1, number];

tuple = [1, 2];
tuple = [1, 3];
tuple = [2, 3]; // Error - TS2322: Type '2' is not assignable to type '1'.
  • 튜플이 '고정된 길이'라는 것은 타입을 선언할 때만 국한된다.
    튜플에 값을 할당할 때는 .push().splice() 등으로 더 많은 값을 추가할 수 있다.
    (맨 위처럼 그냥 수동으로 넣는 건 안 되고 push나 splice 같은 메서드를 사용해야)
    주의할 점은 기존에 선언에 사용된 타입만 추가가 가능하다.
    (string과 number만 포함된 튜플에 boolean 타입은 추가 불가능)
let tuple: [string, number];

tuple = ['a', 1];
tuple = ['b', 2];

tuple.push(3);
console.log(tuple); // ['b', 2, 3] (가능)

tuple.push(true); // (불가능) Error - TS2345: Argument of type 'true' is not assignable to parameter of type 'string | number'.
  • 튜플을 readonly로 선언할 수도 있다. (readonly - 재할당 불가능)
let a: readonly [string, number] = ['Hello', 123];
a[0] = 'World'; // Error - TS2540: Cannot assign to '0' because it is a read-only property.

열거형 (Enum)

  • 숫자 또는 문자열 집합에 이름(Member)을 부여할 수 있는 타입
  • 값의 종류가 일정한 범위로 정해져 있는 경우 유용 (예: 요일)
enum Week {
  Sun,
  Mon,
  Tue,
  Wed,
  Thu,
  Fri,
  Sat
}
  • 수동으로 값 변경이 가능한데, 이때 값을 변경한 부분부터 다시 1씩 증가한다.
  • 역방향 매핑(Reverse Mapping) 지원
    = 열거된 멤버(이름)로 값에 접근, 또는 값을 통해 멤버에 접근 (양방향 참조가 가능하다)
console.log(Week.Sun); // 0
console.log(Week['Sun']); // 0
console.log(Week[0]); // 'Sun'
  • 숫자뿐만 아니라 문자열로도 초기화가 가능하다.
    단, 이 방법은 역방향 매핑(Reverse Mapping)을 지원하지 않으며, 개별적으로 초기화해야 한다.
enum Color {
  Red = 'red',
  Green = 'green',
  Blue = 'blue'
}

console.log(Color.Red); // red
console.log(Color['Green']); // green

any (모든 타입)

  • 타입스크립트의 최상위 타입 중 하나
  • 어떤 타입도 할당 가능하다는 의미
  • 일반 자바스크립트 변수와 똑같아지므로 타입스크립트를 쓰는 의미가 없어진다. (대부분의 경우 권장하지 않음)
  • any 사용을 금지하고 싶을 경우: tsConfig 파일의 compilerOptions에 "noImplicitAny": true로 설정하면 any를 사용할 때 에러가 발생한다.
  • 용도: 타입을 단언하기 어려울 때, JSON.parse 메서드(리턴 타입을 추론할 수 없어서 any 사용)

Unknown (알 수 없는 타입)

  • 타입스크립트 최상위 타입 중 하나
  • any처럼 어떤 값도 할당할 수 있지만, unknown인 값을 다른 타입에 할당할 수는 없다.
  • 일반적으로 타입 단언(Assertions)이나 타입 가드(Guards)와 같은 처리가 필요
  • 더 구체적 타입을 쓰는 걸 권장하며, 불가피한 상황에서는 any보다는 unknown을 쓰는 게 낫다.
let a: any = 123;
let u: unknown = 123;

let v1: boolean = a; // any 타입은 어디든 할당 가능
let v2: number = u; // Error: unknown 값은 any 값을 제외한 다른 곳에는 할당 불가
let v3: any = u; // OK
let v4: number = u as number; // OK: 타입 단언을 사용하면 다른 타입에도 unknown 값을 할당 가능
type Result = {
  success: true,
  value: unknown
  // Union으로 타입 합성 (type aliases)
} | { 
  success: false,
  error: Error // 타입스크립트에서 new Error의 타입은 Error
}

export default function getItems(user: IUser): Result {
  if (id.isValid) {
    return {
      success: true,
      value: ['Apple', 'Banana'] // unknown 자리에 배열 할당
    };
  } else {
    return {
      success: false,
      error: new Error('Invalid user.')
    }
  }
}

Object (객체)

  • 타입스크립트 최상위 타입 중 하나
    (=> 대부분의 타입을 포함하기 때문에 타입 정의에 그다지 유용하지 않음)
  • 기본적으로 null을 포함
    (tsconfig 파일의 컴파일러 옵션에 strictNullChecks: true를 설정하면 null을 포함하지 않는다)
let obj: object = {};
let arr: object = [];
let func: object = function () {}; // 함수 포함
let nullValue: object = null; // null 포함
let date: object = new Date(); // 날짜 객체 포함

객체와 배열 인덱싱할 때 주의점

타입스크립트는 배열을 참조할 때 컴파일 단계에서 에러를 모를 수 있다.

const l = [1, 2, 3]
const item = l[3]
item + 1 // error이지만 컴파일 때는 모름

const user: { [key: string]: string } = { name: 'kyc' }
user.age + 1 // 마찬가지로 error이지만 컴파일 때 모른다

이를 체크하는 방법은 컴파일러 옵션의 noUncheckedIndexedAccess (But 완벽하지 않음)

const l = [1, 2, 3]
const item1 = l[3]
const item2 = l[2]

item1 + 1 // Object is possibly 'undefined'.(2532)
item2 + 1 // 근데 문제는 이거까지 에러가 난다는 거다

l.map((item) => item + 1) // map으로 돌려서 하는 연산에서는 괜찮음

noUncheckedIndexedAccess는 이처럼 적당히 경고를 날려주는 장점도 있지만 그다지 똑똑하지 않다는 점도 있다.

결론적으로 객체나 리스트의 값을 인덱싱(참조)할 때는 undefined가 있을 가능성에 대해 염두해 두어야 한다.

Null과 Undefined

  • 기본적으로 모든 타입의 하위 타입 (대부분의 경우 할당 가능)
    심지어 서로의 타입에도 할당 가능
let num: number = undefined;
let str: string = null;

let obj: { a: 1, b: false } = undefined;
let arr: any[] = null;

let und: undefined = null;
let nul: null = undefined;

let voi: void = null;
  • 컴파일러 옵션의 strictNullChecks: true을 설정하면 null과 undefined 서로의 타입까지 엄격하게 할당할 수 없게 할 수 있다.
  • 단, void에는 여전히 undefined 할당 가능
let voi: void = undefined; // Ok

Void

  • 값을 반환하지 않는 함수의 반환 값
  • 값을 반환하지 않는 함수는 실제로는 undefined 반환
function hello(msg: string): void {
  console.log(`Hello ${msg}`);
}

const hi: void = hello('world'); // Hello world
console.log(hi); // undefined

Never

  • 절대 발생하지 않을 값의 타입
  • 어떠한 타입의 값도 할당할 수 없음
  • 사용 예: 실행문이 new Error인 함수의 반환 값, 빈 배열을 타입으로 잘못 선언한 경우 등
function error(message: string): never {
  throw new Error(message);
}

const never: [] = []; // 타입이 빈 배열일 수는 없다
never.push(3); // Error - TS2345: Argument of type '3' is not assignable to parameter of type 'never'.

타입 추론 (Inference)

  • 명시적으로 타입이 선언되지 않은 경우 타입스크립트가 타입을 추론하는 것
  • 엄격하지 않은 타입 선언을 의미하는 것은 아니다.
  • 많은 경우에 더 좋은 코드 가독성을 제공
  • 타입 추론이 되는 예: 초기화된 개별 변수, 기본값이 설정된 함수의 매개 변수, 함수의 반환 값
// 초기화된 변수 `num`
let num = 12;

// 기본값이 설정된 매개 변수 `b`
function add(a: number, b: number = 2): number {

  // 함수의 반환 값(`a + b`)
  return a + b;
}

타입 단언 (Assertions)

  • 타입스크립트가 타입 추론을 통해 판단할 수 있는 범주를 넘는 경우
  • 개발자가 타입스크립트보다 타입에 대해 더 잘 알고 있음을 가정함

예제

val은 string이나 number, isNumber는 boolean이다.
개발자는 isNumber가 앞에 'is'가 붙은 네이밍을 통해 숫자 여부를 판단하는 값임을 알 수 있다.
타입스크립트는 ‘isNumber’라는 이름만으로는 이 내용을 추론할 수 없다.

function someFunc(val: string | number, isNumber: boolean) {
  if (isNumber) {
  	// toFixed 메서드: 소수 자릿수로 반올림 후 string으로 변환하는 메서드 - 앞에 number만 가능
    val.toFixed(2); // Error - TS2339: ... Property 'toFixed' does not exist on type 'string'.
    // val이 string인지 number인지 모르는 타입스크립트는 toFixed를 객체의 프로퍼티로 추론해버림
  }
}

타입 단언으로 이 문제를 해결할 수 있다.

function someFunc(val: string | number, isNumber: boolean) {
  if (isNumber) {
    (val as number).toFixed(2); // as로 단언 후 괄호로 묶어줌
  }
}
  • API 호출에서도 사용된다.
const response = await fetch('/api/user')
const result = (await response.json()) as UserInterface
  • 타입 가드로 예외 및 에러 처리를 해줄 수 있다.
function isUser(data: unknown): data is UserInterface {
  return data && typeof data === 'objet' && 'name' in data // ...
}

const response = await fetch('/api/user')
const result = await response.json()

if (!isUser(result)) {
  throw new Error(`${result}는 UserInterface가 아님`)
}

타입 단언은 마치 프로그래머가 타입스크립트에게 “나는 알고 있으니까 나를 믿어!”라고 알려주는 것과 같다.

Non-null 단언 연산자 ('!' 사용)

  • 피연산자가 Nullish 값(= null, undefined)이 아님을 단언

예제

매개 변수 x가 null이나 undefined일 수 있어서 에러가 발생하는 경우

function fnA(x: number | null | undefined) {
  return x.toFixed(2); // Error - TS2533: Object is possibly 'null' or 'undefined'
}

if 조건문이나 타입 단언으로 해결할 수도 있지만,
마지막처럼 Non-null 단언 연산자를 이용하여 간단히 처리할 수 있다.

// if 조건문
function fnD(x: number | null | undefined) {
  if (x) { // true이면 = 존재하면 = null이나 undefined가 아니면
    return x.toFixed(2);
  }
}

// 타입 단언
function fnB(x: number | null | undefined) {
  return (x as number).toFixed(2);
}

// Non-null 단언 연산자
function fnE(x: number | null | undefined) {
  return x!.toFixed(2); // 위의 내용을 간단하게 표현 가능
}

타입 가드 (Guards)

예제

val의 타입을 매번 보장하기 위해 타입 단언을 여러 번 사용한 경우

function someFunc(val: string | number, isNumber: boolean) {
  if (isNumber) {
    (val as number).toFixed(2);
    isNaN(val as number);
  } else {
    (val as string).split('');
    (val as string).toUpperCase();
    (val as string).length;
  }
}

타입 가드를 사용하면 타입스크립트가 추론 가능한 타입의 특정 범위를 보장할 수 있다.

방법 1. 반환 타입을 술부(Predicate)로 선언

반환 값으로 NAME is TYPE 라는 술부를 가진 함수를 생성 후 적용

// val이 number일 경우 val의 타입을 number로 확정해주는 함수를 따로 선언
function isNumber(val: string | number): val is number {
  return typeof val === 'number'; // typeof 사용
}

function someFunc(val: string | number) {
  if (isNumber(val)) { // isNumber 함수 적용
    val.toFixed(2);
    isNaN(val);
  } else {
    val.split('');
    val.toUpperCase();
    val.length;
  }
}

그 외 방법들

함수를 따로 만들지 않고 간편하게 처리하기

  • 🏓 typeof 키워드 (number, string, boolean, symbol만 타입 가드로 인식)
  • 🏓 in 연산자 (우변 객체(val)가 any 타입이어야)
  • instanceof 연산자
// typeof
function someFuncTypeof(val: string | number) {
  if (typeof val === 'number') {
    val.toFixed(2);
    isNaN(val);
  } else {
    val.split('');
    val.toUpperCase();
    val.length;
  }
}

// in
function someFuncIn(val: any) {
  if ('toFixed' in val) { // toFixed가 val에 있으면 val이 number라는 뜻
    val.toFixed(2);
    isNaN(val);
  } else if ('split' in val) { // split이 val에 있으면 val이 string이라는 뜻
    val.split('');
    val.toUpperCase();
    val.length;
  }
}

// instanceof
class Cat {
  meow() {}
}
class Dog {
  woof() {}
}
function sounds(animal: Cat | Dog) {
  if (animal instanceof Cat) {
    animal.meow();
  } else {
    animal.woof();
  }
}

개인적인 생각으로는 typeofin을 쓰는 게 간단할 것 같다.

인터페이스 (Interface)

확장 가능

기존에 만들어진 인터페이스에 내용 추가

방법 1. extends로 상속

interface IAnimal {
  name: string
}
interface ICat extends IAnimal {
  meow(): () => string
}

const catInfo: ICat = {
	name: 'Kitty',
    meow(): () => 'Meowwww'
}

방법 2. 동일 인터페이스명으로 중복 선언

interface IFullName {
  firstName: string,
  lastName: string
}
interface IFullName { // 같은 이름으로 추가 타입을 선언하면 확장됨
  middleName: string
}

const fullName: IFullName = {
  firstName: 'Tomas',
  middleName: 'Sean',
  lastName: 'Connery'
};

class 타입 지정

인터페이스로 class의 타입을 정의하는 경우 implements 키워드를 사용

interface IUser {
  name: string,
  getName(): string
}

class User implements IUser {
  constructor(public name: string) {}
  getName() {
    return this.name;
  }
}

const neo = new User('Neo');
neo.getName(); // Neo

타입 별칭 (Type Aliases)

  • type 키워드와 union 연산자로 둘 이상의 타입을 조합할 수 있다.
    앞에 Type를 의미하는 T를 붙여서 타입 네이밍 선언
type TUser = {
  name: string,
  age: number,
  isValid: boolean
} | [string, number, boolean];

let userA: TUser = {
  name: 'Neo',
  age: 85,
  isValid: true
};

let userB: TUser = ['Evan', 36, false];

제네릭 (Generic)

  • 함수나 클래스의 선언 시점이 아닌, 사용 시점재사용을 목적으로 타입을 선언할 수 있는 방법
  • 타입을 인수로 받아서 활용한다.
  • union 방법보다 가독성이 좋아지고, 인수의 타입을 유연하게 받을 수 있다.

예제

두 가지 값을 인수로 받는 toArray 함수에서 number 타입 선언이 여러 번 이루어지고 있다.
여기서 string 타입을 넣으면 에러가 발생한다.

function toArray(a: number, b: number): number[] {
  return [a, b];
}

toArray(1, 2);
toArray('1', '2'); // Error - TS2345: Argument of type '"1"' is not assignable to parameter of type 'number'.

union으로 타입 확장하기

string도 허용되도록 union을 이용해 인수 선언을 확장했다.
가독성이 떨어지고, 세 번째 호출의 경우 의도치 않게 number와 string 타입을 동시에 받게 되었다.

function toArray(a: number | string, b: number | string): (number | string)[] {
  return [a, b];
}
toArray(1, 2); // Only Number
toArray('1', '2'); // Only String
toArray(1, '2'); // Number & String

Generic 사용

함수명 옆에 제네릭 타입 <T>를 작성하고 각 인수와 반환 값의 타입에도 T를 할당했다.

T는 타입 변수(Type variable)개발자가 제공한 타입으로 변환된다.

기본적으로 타입 추론이 되지만, 세 번째처럼 인수에 서로 다른 타입이 들어올 경우에는 union 타입으로 각각 타입을 명시해줘야 한다.

function toArray<T>(a: T, b: T): T[] {
return [a, b];
}

toArray(1, 2);
toArray('1', '2');
toArray<string | number>(1, '2'); // union으로 타입 명시

부가적 개념

keyof

  • 객체의 프로퍼티(key)명을 union으로 묶인 값 타입 조합으로 만들 수 있다.
interface ICountries {
  KR: '대한민국',
  US: '미국',
  CP: '중국'
}

let country: keyof ICountries; // 'KR' | 'US' | 'CP'
country = 'KR'; // ok
country = 'RU'; // Error - TS2322: Type '"RU"' is not assignable to type '"KR" | "US" | "CP"'.
  • 대괄호('[ ]') 인덱싱으로는 타입의 값(value)에 접근 가능하다.
let country: ICountries[keyof ICountries]; // ICountries['KR' | 'US' | 'CP'] = '대한민국' | '미국' | '중국'

country = '대한민국';
country = '러시아'; // Error - TS2322: Type '"러시아"' is not assignable to type '"대한민국" | "미국" | "중국"'.

유틸리티 타입

T는 타입(Type), U는 또 다른 타입, K는 key를 의미하는 약어

Partial 🎀

  • T의 모든 속성을 선택적(optional)으로 변경 (인터페이스)
  • Partial<T>

Required 🎀

  • T의 모든 속성을 필수(required)로 변경 (인터페이스)
  • Required<T>

Readonly 🎀

  • T의 모든 속성을 읽기 전용(readonly)으로 변경 (인터페이스)
  • Readonly<T>

Record

  • K를 프로퍼티로, T를 프로퍼티의 타입으로 매핑 (인터페이스)
  • Record<K, T>

Pick 🎀

  • T의 속성 중 K만 선택해서 반환 (인터페이스)
  • Pick<T, K>

Omit 🎀

  • T의 속성 중 K만 제외하고 반환 (인터페이스)
  • Omit<T, K>
interface Todo {
  title: string;
  description: string;
  completed: boolean;
  createdAt: number;
}

type TodoPreview = Omit<Todo, 'description'>;

const todo: TodoPreview = {
  title: 'Clean room',
  completed: false,
  createdAt: 1615544252770,
};

Exclude 🎀

  • union으로 연결된 T 타입 중 특정 타입 U만 제외하고 반환
  • Exclude<T, U>
type T0 = Exclude<'a' | 'b' | 'c', 'a'>; // a, b, c 중에 a 제거
type T0 = 'b' | 'c';

Extract 🎀

  • union으로 연결된 T 타입 중 특정 타입 U만 선택해서 반환
  • Extract<T, U>

NonNullable 🎀

  • union으로 연결된 T 타입 중 nullundefined만 제외하고 반환
  • NonNullable<T>

Parameters

  • T의 매개변수 타입을 새로운 튜플 타입으로 반환 (함수, 튜플)
  • Parameters<T>

ConstructorParameters

  • T의 매개변수 타입을 새로운 튜플 타입으로 반환 (클래스, 튜플)
  • ConstructorParameters<T>

ReturnType 🎀

  • T의 반환 타입을 새로운 타입으로 반환 (함수)
  • ReturnType<T>

InstanceType

  • T의 인스턴스 타입을 반환 (클래스)
  • InstanceType<T>

ThisParameterType

  • T의 명시적 this 매개 변수 타입을 새로운 타입으로 반환 (함수)
  • ThisParameterType<T>

OmitThisParameter

  • T의 명시적 this 매개변수를 제거한 새로운 타입을 반환 (함수)
  • OmitThisParameter<T>

ThisType

  • T의 this 컨텍스트(context)를 명시
  • 별도 반환 없음 (인터페이스)
  • ThisType<T>

참고 링크

한눈에 보는 타입스크립트(updated)
[Typescript] 기존 React + js 프로젝트 ts로 바꾸기
타입스크립트에서 조심해야 할 습관
A difference between TypeScript Omit and exclude

profile
데이터 분석가 준비 중입니다 (티스토리에 기록: https://cherylog.tistory.com/)

0개의 댓글