Post

[ts] 에러를 사전에 방지하기 위한, 타입스크립트와 스펙을 정의하는 ts의 인터페이스

타입스크립트를 사용하는 이유

🐁 타입스크립트는 자바스크립트에 타입을 부여한 언어이다

데이터에 타입이 정의되어 있을 때, 다음과 같은 효과를 볼 수 있다.

  • 🎯 에러를 사전에 방지
  • 🎯 코드 가이드 및 자동완성
    해당 타입이 제공하는 api를 preview로 노출해준다.

js와는 다르게 ts는 브라우저에서 실행하기 위해 파일을 브라우저가 인식할 수 있는 형태의
js 파일로 변환해 주어야 하는데, 이러한 변환과정을 컴파일이라 한다.

tsc (tyscript compiler) 명령어를 실행하기 위해서 관련 라이브러리를 설치해야 한다.
npm i typescript -g

타입이 정의되지 않은 코드에 타입을 입혀주는 것을 타이핑 (typing)이라고 하며,
타입 어노테이션은 : 이다.

ts의 타입에는 문자열, 숫자, 배열, 튜플, 객체, 불리언, any, void, never 등이 있다.
any 타입은 모든 타입을 통칭하는데, 해당 타입으로 정의하는 것은 별 의미가 없다.
변수에 대한 구체적인 타입을 정의하는게, 타입스크립트를 사용하는 이유이자 에러를 줄이는 방법이다.

1
2
3
4
5
6
7
8
9
10
11
// 배열
let arr: number[] = [1, 2, 3]; // let arr: Array<number> = [1, 2, 3]

// 튜플: 배열의 특정 인덱스 타입까지 정의
let address: [string, number] = ["gangnam", 100];

// 객체
let obj: object = {
  name: "peter",
  age: 30
};

객체의 타입을 정의할 때 위와같이 object로 정의해도 되지만,
이렇게 정의하면 객체의 형상이 드러나지 않는다.

1
2
3
4
5
// 객체의 형상이 드러나게 정의
let obj: { name: string; age: number } = {
  name: "peter",
  age: 30
};
📕 이넘(enum) 타입
특정값들의 집합을 의미하는 자료형이다.
별도의 값으로 초기화하지 않으면 숫자형 이넘(enum)으로 취급한다.

인자를 이넘(enum)으로 정의하면 이넘(enum)에서 제공하는 값만을 넘길 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
enum Answer {
  Yes = "Y",
  No = "N"
}

function askQuestion(anser: Answer) {
  if (answer == Answer.Yes) {
    console.log("정답입니다.");
  }

  if (answer == Answer.No) {
    console.log("오답입니다.");
  }
}

askQuestion(Answer.Yes); // (ㅇ)
askQuestion("Yes"); // error!
askQuestion("Y"); // error!

js 유연함 / ts 엄격함

🥯 js 유연함  /  ts 엄격함

자바스크립트에서는 정의된 함수 스펙의 파라미터 개수보다 많은 인자를 넣어 호출해도 문제가 안된다.
이를 자바스크립트의 유연함이라 하는데, 타입스크립트는 이를 허용하지 않는다.

대신에, 타입스크립트는 옵셔널 파라미터 ? (= 선택적 파라미터)를 제공하는데,
이는 타입스크립트에 어느정도의 유연함을 제공한다.

1
2
3
function logText(a: string, b?: string, c?: string) {
  // ...
}

타입정의 시 ?를 추가적으로 붙인 인자는 호출할 때, 있을 수도 있고, 없을 수도 있다는 것을 뜻한다.

타입스크립트 설정파일, tsconfig.json

🐙 매번 tsc 명령어를 돌리는 것보다는, 웹팩으로 반복적으로 사용하는 명령어를 자동화할 수 있다

ts 명령어를 쓸 때, 부가적인 옵션을 타입스크립트 설정파일 🎳 tsconfig.json에 설정해둘 수 있다.

1
2
3
4
5
6
7
8
9
10
{
  "compilerOptions": {
    "allowJs": true,
    "checkJs": true,
    "noImplicitAny": true,
    "strict": true,
    "strictFunctionTypes": true
  },
  "include": ["./src/**/*"]
}
  • checkJs: true
    @ts-check, 타입스크립트의 타입검사 기능을 사용하겠다.
  • noImplicitAny
    명시적으로 기본적인 타입인 any라도 정의해야 한다.
  • strict
    타입스크립트의 엄격한 타입검사를 강제한다.

스펙(구조)을 정의하는 인터페이스

🍝 객체의 형상을 일일이 나열해서 각각 타입을 정의하면, 중복코드가 많아진다.

🗽 중복된 코드를 줄이기 위해 객체의 스펙을 정의하면 된다.

(1) 타입 별칭 (type) 사용
새로운 타입을 생성하는 것이 아닌, 정의된 구조에 이름을 그저 부여하는 것이다.
즉, type을 사용하는 것은 특정 타입이나, 인터페이스를 참조할 수 있는 타입변수를 만드는 것이다.
1
2
3
4
5
6
7
var Name = string;
const myName: Name = "peter";

type Person = {
  name: string;
  age: number;
};
(2) 인터페이스 (interface) 사용
새롭게 정의한 인터페이스 타입을 생성한다.
extends 키워드를 사용해 확장(상속)할 수 있다.
1
2
3
4
5
6
7
8
interface Person {
  name: string;
  age: number;
}

interface Developer extends Person {
  skill: string;
}

인덱싱 방식을 인터페이스에 정의할 수 있다.
그때그때마다 속성(index, key)을 타입만 맞도록, 임의로 부여해서 사용할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
interface StringArr {
  [index: number]: string;
}

// 인터페이스 딕셔너리 패턴
interface StringRegDictionary {
  [key: string]: RegExp; // 정규표현식 타입
}

var obj: StringRegDictionary = {
  cssFile: /\.css$/,
  jsFile: /\.js$/
};

함수의 스펙(구조)에 대해서도 인터페이스에 정의할 수 있다.

1
2
3
4
5
6
7
interface SumFn {
  (a: number, b: number): number;
}

var sum: SumFn = function (a: number, b: number): number {
  return a + b;
};

type은 상속이 안되지만, interface는 상속이 가능하다.
따라서, type보다는 interface를 사용하는 것을 권장한다.
⚠️ 좋은 소프트웨어는 언제나 확장이 용이해야한다는 원칙을 따른다.

유니온 타입과 공통속성

🌋 유니온 타입을 이용해서 하나 이상의 타입을 정의할 수 있다.

☄️ 유니온 타입 | 을 쓰면 공통속성에만 접근할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
interface Person {
  name: string;
  age: number;
}

interface Developer {
  name: string;
  skill: string;
}

function askSomeone(someone: Person | Developer) {
  console.log(someone.name); // (ㅇ)
  console.log(someone.skill); // error!
}

유니온을 이용해 타입을 정의한 인자에는 🐖 확실하게 둘 중 어떤 타입이 들어올지 모른다.
혹여나, 공통속성이 아닌 다른 속성을 쓸 수 있게 한다면 🐖 타입세이프하지 않은, 즉 에러가 날 수 있는 여지가 있기 때문에 이를 막아놨다.

만일, 나머지 속성을 이용하고 싶으면 타입가드에 대한 추가적인 처리가 필요하다.

🍍 타입가드
특정타입으로 타입의 범위를 좁혀나가는 과정

🗿 type-guard.ts

1
2
3
4
5
6
7
8
9
10
11
function askSomeone(someone: Person | Developer) {
  console.log(someone.name);
  if (typeof someone == "Person") {
    console.log(someone.age);
  }
  if (typeof someone == "Developer") {
    console.log(someone.skill);
  }

  throw new TypeError("someone must be Person or Developer");
}
🍍 인터섹션 타입 &
인터섹션을 이용해 타입을 정의한 인자에는 정의한 타입이 가지고 있는 모든 속성을 포함하는 인자만 올 수 있다.
결과적으로는, 두개 이상의 인터페이스를 합친 새로운 인터페이스 타입을 생성하게 된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const nth: string & number & boolean; // :never

function askSomeone(someone: Person & Developer) {
  console.log(someone.name);
  console.log(someone.age);
  console.log(someone.skill);
}

const someone = {
  name: "peter",
  age: 30,
  skill: "ts"
};

askSomeone(someone);

ES6, 클래스와 프로토타입의 관계

🐀 자바스크립트는 프로토타입 기반의 언어이다

🎯 클래스를 사용하는 이유

🐝 class-ex.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const user_before = { name: "peter", age: 30 };
const admin_before = { name: "jack", age: 50, role: "admin" };

class User {
  constructor(name, age) {
    // 초기화 메서드 (생성자)
    console.log("--created--");
    this.name = name;
    this.age = age;
  }
}

class Admin extends User {
  constructor(name, age, role) {
    super(name, age);
    this.role = role;
  }
}

const user = new User("peter", 30);
const admin = new Admin("jack", 50, "admin");

클래스는 상속, 캡슐화, 다형성 같은 객체 지향 프로그래밍의 주요 개념을 지원하여 🏆 재사용 가능한 코드를 작성할 수 있게 해준다.

또한, 관련있는 속성과 메서드를 한 곳에 모아 논리적 단위로 관리할 수 있고 (모듈화),
접근자를 통해 데이터를 캡슐화할 수 있다.

정적 메서드를 이용해, 유틸리티 함수를 만드는데 유용하다.

🐝 util.js

1
2
3
4
5
6
7
class MathUtils {
  static add(a, b) {
    return a + b;
  }
}

console.log(MathUtils.add(3, 5));

js에서 프로토타입은 클래스 문법이 도입되기 전부터 사용되었다.
모든 객체는 자신의 프로토타입을 참조하고, 이를 통해 상속이 이루어진다.

🐝 prototype-ex.js

1
2
3
4
5
6
7
8
9
10
11
12
function User(name, age) {
  this.name = name;
  this.age = age;
}

// 모든 함수는 자동으로 prototype 속성을 가지며, 이를 통해 메서드를 정의한다.
User.prototype.greet = function () {
  console.log(`Hi, my name is ${this.name}`);
};

const user = new User("peter", 30);
user.greet();

객체는 __proto__ 또는 [[Prototype]]을 통해 부모 객체(프로토타입)에 접근한다.
js의 객체는 내부적으로 [[Prototype]]이라는 링크를 가지고 있다.
이 링크를 통해 상위 객체를 참조하며, __proto__이 링크를 노출한 속성이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 예시1
const obj = { a: 10 };
obj;
// a: 10
// [[Prototype]]: Object

// 기본적으로 객체를 만들면 상위 prototype이 Object라는 최상위 객체가 있다.
// 때문에 Object 객체 안의 속성 및 메서드를 사용할 수 있다.

const arr = [];
arr;
// length: 0
// [[Prototype]]: Array(0)

// 예시2
const user = { name: "peter", age: 30 };
const admin = {};

admin.__proto__ = user; // 프로토타입의 상위에 user 객체를 정의
amdin.role = "admin";

console.log(admin.name); // 상위 오브젝트 속성 및 메서드를 가져다 쓸 수 있다.

// 예시3
const parent = {
  greet() {
    return "Hello from parent!";
  }
};

const child = Object.create(parent); // child의 [[Prototype]]이 parent를 참조
console.log(child.greet()); // 프로토타입 체인을 따라 child.greet() 호출 가능
console.log(child.__proto__ === parent); // true

사실 js의 클래스도 내부적으로 프로토타입을 기반으로하고, 프로토타입 기반의 상속도 유지한다.
즉, 클래스는 단순히 프로토타입을 간편하게 추상화한 문법적 설탕 (Syntactic Sugar)이다.

타입스크립트 클래스 문법

🐙 타입스크립트 클래스 문법

ts의 클래스에서는 사용할 멤버변수를 정의해야하며,
변수의 접근범위나 유효동작을 지정할 수 있다. default, public

1
2
3
4
5
6
7
8
9
10
clas User {
  private name: string;
  private age: number;
  readonly log: string;

  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }
}

다만, 점차 클래스 기반 코드를 볼일이 없어진다.

  • react
    • 예전문법: 클래스 기반 코드 extends React.Component
    • 최신문법: hook 기반의 함수형 코드
  • vue Composition API
This post is licensed under CC BY 4.0 by the author.