TypeScript

<타입스크립트 프로그래밍> 3장: 타입의 모든 것

sqsung 2023. 5. 26. 12:43

3장: 타입의 모든 것

img

3-1. 타입을 이야기하다

  • 간단한 예시지만, 인수를 제곱해서 반환하는 함수가 있다고 가정해봤을 때, 만약 숫자 타입이 아닌 인수를 전달받게 되면 유효하지 않은 작업을 수행하게 된다
  • 예를 들어 자바스크립트에서는 매개변수로 문자열 타입인 'z'를 입력해도, NaN을 반환하는 것 외에는 별 다른 타입 관련 에러가 발생하지 않는다
  • 타입스크립트에서는 애초에 매개변수의 타입을 'Number'로 제한해두어, 잘못된 매개변수가 전달되는 것을 방지할 수 있다 (함수를 실행시키지 않아도 잘못된 매개변수가 입력됨을 알 수 있다)
function squareOf(n: number) {
  return n * n;
}

console.log(squareOf(2)); // 4
console.log(squareOf('z')); // Argument of type 'string' is not assignable to parameter of type 'number'.

3-2. 타입의 가나다

3-2-1. any

  • any 타입은 무엇이든 할 수 있지만, 꼭 필요한 상황이 아니라면 사용하지 않는 것이 좋다
  • any 타입을 사용한다는 것은 곧 값이 자바스크립트처럼 동작하기 시자하고, 타입 검사기의 의미가 없어진다
  • 타입스크립트 기본 설정은 any로 추론되는 값을 발견하더라도 예외를 발생시키지 않지만, 암묵적인 any가 나타났을 때 예외를 일으키고 싶다면 tsconfig.json 파일에 noImplicitAny 플래그를 활성화하면 된다 (strict를 활성화 했다면 따로 하지 않아도 된다)

3-2-2. unknown

  • 타입을 알 수 없는 어떤 값이 있을 때 any 대신 unknown을 사용하는 것이 좋다
  • unknown도 모든 값을 대표하지만, unknown 타입을 검사해 정제하기 전까지는 타입스크립트가 unknown 타입의 값을 사용할 수 없게 강제한다
let a: unknown = 30;

let b = a + 10; // Error: 'a' is of type 'unknown'

3-2-3. boolean

  • boolean 타입은 참(true), 거짓(false) 두 개의 값을 나타낸다
  • 당연히 어노테이션을 사용해 특정 값을 boolean으로 명시할 수도 있지만, 타입 리터럴 기능으로 특정 불리언 값만 지정할 수도 있다
let firstBoolean: true = true;
let secondBoolean: false = false;

console.log(typeof firstBoolean); // 'boolean'

secondBoolean = true; // Error: Type 'true' is not assignable to type 'false'.
  • 또한, const 키워드를 통해 선언된 변수에 boolean 값 중 하나를 할당하는 경우, 타입스크립트는 변수가 바뀔 일이 없음을 인지하고 해당 변수가 가질 수 있는 가장 좁은 타입으로 추론한다
const thirdBoolean = true; // True

3-2-4. number

  • number 타입은 모든 숫자(정수, 소수, 양수, 음수, Infinity, NaN 등)의 집합이며, 덧셈, 뺼셈, 모듈로, 비교 등의 숫자 관련 연산을 수행할 수 있는 값을 나타낸다
  • boolean처럼 개발자들은 대개 타입스크립트가 직접 추론하도록 한다
  • boolean과 마찬가지로 const 키워드로 선언된 변수에 특정 숫자를 할당하는 경우, 타입스크립트는 해당 변수의 타입을 number가 아닌 특정 숫자로 추론한다
let a = 1; // Number
let b = 2; // Number
const firstNumber = 100; // 100

3-2-5. bigint

  • number 타입은 225 까지의 정수를 표현할 수 있지만, bigint를 이용하면 이보다 큰 수도 표현할 수 있다
  • boolean, number 타입과 마찬가지로 bigint 타입은 타입스크립트가 직접 추론하도록 하는 게 일반적이다

3-2-6. string

  • 모든 문자열의 집합으로 연결(+), 슬라이스(.slice) 등의 연산을 수행할 수 있다
  • boolean, number, bigint와 마찬가지로 타입스크립트가 추론하도록 두는 것이 좋다

3-2-8. 객체

  • 타입스크립트의 객체 타입은 객체의 형태를 정의한다
  • 재미있게도 객체 타입만으로는 ({}로 만든) 간단한 객체와 (new연산자를 사용해 만든) 복잡한 객체를 구분할 수 없다. 이는 자바스크립트가 구조 기반 타입을 갖도록 설계되었기 때문이다
💡구조 기반 타입화 (Structural Type)

구조 기반 타입화에서는 객체의 이름에 상관없이 객체가 어떤 프로퍼티를 갖고 있는지를 따진다

반대로 '이름 기반 타입'에서는 이름을 따진다
  • 타입스크립트에서 객체를 서술하는 데 타입을 이용하는 방식은 여러 가지다. 첫 번째 방법은 값을 object로 선언하는 것이다
let test: object = {
  a: 'x',
};

console.log(test.a); // Property 'a' does not exist on type 'object'.
  • 단, 이렇게 하는 경우 값의 프로퍼티에 접근할 수 없다
  • 사실 objectany 보다 좁은 타입이다. object는 서술하는 값에 관한 정보를 거의 알려주지 않으며, 값 자체가 자바스크립트 객체라고, 그리고 null이 아니라고 말해줄 뿐이다
  • 대신 명시적으로 정의하지 않고, 타입스크립트가 추론하도록 하면 아래처럼 동작한다
let test = {
  a: 'x',
}; // { a: string }

console.log(typeof test.a); // --> string
  • 이게 두 번째 방법인, '객체 리터럴' 방법이다
  • 타입스크립트가 test의 형태를 추론하게 하거나, 중괄호 안에 명시적으로 타입을 묘사할 수 있다
  • 타입스크립트는 객체 프로퍼티에 엄격한 편이다. 예를 들어 객체에 number 타입의 b라는 프로퍼티가 있어야 한다고 정의하면, 타입스크립트는 오직 b만을 기대한다. b가 없거나 다른 추가 프로퍼티가 있으면 에러를 발생시킨다
  • 물론 어떠 프로퍼티는 선택형이고, 예정에 없던 프로퍼티가 추가될 수 있다고 타입스크립트에 알려줄 수 있다
let testObject: {
  b: number; // 1번. 숫자 타입의 'b' 프로퍼티는 무조건 포함한다
  c?: string; // 2번. 문자열 타입의 'c' 프로퍼티는 포함되어 있을 수도 있다 (선택형)
  [key: number]: boolean; // 3. 숫자 타입의 키와 불리언 타입의 값의 프로퍼티를 여러 개 포함할 수 있다 (추가 가능)
};
  • 3번에서처럼 [key: T]: U 같은 문법을 인덱스 시그니처Index Signature라고 부른다. 이 경우 "모든 T타입의 키는 U타입의 값을 갖는다"라고 해석할 수 있다
  • '빈 객체' 타입이나 '객체: Object'로 객체를 만드는 방법도 있지만, 가능하면 사용하지 않는 것이 권장된다

3-2-9. 휴식 시간: 타입 별칭, 유니온, 인터섹션

A. 타입 별칭:

  • let, const, var 등의 키워드로 값 대신 변수로 칭하듯이, 타입 별칭으로 타입을 가리킬 수 있다
  • 아래 예제의 경우 Age 타입은 결국 number타입이지만, 이를 통해 Person 객체의 형태를 조금 더 이해하기 쉽게 정의할 수 있다
  • 타입스크립트는 별칭을 추론하지는 않으므로 반드시 별칭의 타입을 명시적으로 정의해야 한다
  • 변수 선언과 마찬가지로 하나의 타입을 두 번 정의할 수는 없으며, 블록 영역에 적용된다
  • 타입 별칭은 복잡한 타입을 DRY(Don't Repeat Yourself)하지 않도록 해주며, 변수가 어떤 목적으로 사용되었는지 쉽게 이해할 수 있게 도와준다
type Age = number;

type Person = {
  name: string;
  age: Age;
};

B. 유니온과 인터섹션 타입

  • 타입스크립트는 타입에 적용할 수 있는 특별한 연산 유니온|과 인터섹션&을 제공한다
type Cat = { name: string; purrs: boolean };
type Dog = { name: string; barks: boolean; wags: boolean };

type CatOrDogOrBoth = Cat | Dog;
type CatAndDog = Cat & Dog;
  • 위 예제에서 만들어진 CatOrDogBoth 타입은 Cat 타입과 Dog의 유니온으로 만들어졌으며, 타입명에서 보여지듯 Cat 타입, Dog 타입, 혹은 둘 다에 해당할 수도 있다 (아래 예제 참고)
let someSuperAnimal: CatOrDogOrBoth = {
  name: 'James',
  purrs: true,
  barks: true,
  boolean: true,
};

3-2-10. 배열

  • 대개는 배열을 동형homogenous으로, 즉 모든 값의 타입이 동일 통일되도록 설계한다
  • 초기화 단계에 들어있는 값을 기준으로 타입스크립트는 배열이 받을 수 있는 값을 추론한다
const numberArray = [1, 2, 3]; // number[]

const stringArray = ['James']; // string[]

const mixedArray = [1, 'red']; // (number | string)[]

const emptyArray = []; // any[]

3-2-11. 튜플

  • 튜플은 배열의 서브타입으로, 길이가 고정되었고 각 인덱스의 타입이 알려진 배열의 일종이다
  • 다른 타입과 다르게 튜플은 선언할 때 타입을 무조건 명시해야 한다
  • 객체와 마찬가지로 선택형 문법을 지원한다
  • 또한 최소 길이를 갖도록 지정할 때는 나머지 요소...를 사용할 수 있다
let tupleArray: [string, string, number] = ['James', 'Suwon', 23];

// 두 번째 값은 선택형이기 때문에 trainFares, trainFares2 모두 괜찮다
let trainFares: [number, number?] = [3.75];
let trainFares2: [number, number?] = [3.75, 22.22];

// 나머지 연산자 사용
let friends: [string, ...string[]] = ['Steve', 'Connor', 'Paul'];
  • 일반 배열은 갱신 작업을 자유롭게 할 수 있는 반면 readonly 배열의 값은 오직 참조만 가능하다
let nations: readonly string[] = ['Korea', 'Japan', 'USA', 'Mexico', 'UK'];

nations.push('Russia'); // Error: Property 'push' does not exist on type 'readonly string[]'

3-2-12. null, undefined, void, never

타입 의미
null 값이 없음
undefined 아직 값을 변수에 할당하지 않음
void return문을 포함하지 않은 함수
never 절대 반환하지 않는 함수

  • undefined 타입은 "아직 정의하지 않았음"을 뜻하는 반면 null 타입은 "값이 없다"는 의미다
  • 타입스크립트는 추가로 "존재하지 않음"을 더 세밀하게 분류할 수 있도록 voidnever 타입도 제공한다
  • void 타입은 명시적으로 아무것도 반환하지 않는 함수의 반환 타입을 가리키며, never 타입은 절대 반환하지 않는 함수(예외를 던지거나 영원히 실행되는) 타입을 가리킨다
// 1. never을 반환하는 함수
function throwSomeError() {
  throw TypeError('TypeScript is hard, huh?');
}

// 2. void를 반환하는 함수
function calc() {
  let a = 2 + 2;
  let b = a * a;
}

3-2-13. 열거형

  • 열거형enum은 해당 타입으로 사용할 수 있는 값을 열거하는 기법이다 (키가 컴파일 타임에 고정된 객체라고 생각하면 쉽다)
  • 타입스크립트는 자동으로 열거형 각 맴버에 적절한 숫자를 추론해 할당한다 (직접 명시할 수도 있음)
  • 일반 객체처럼 점 또는 괄호 표기법으로 열거형 값에 접근할 수 있다
enum Language {
  Korean,
  English,
  Spanish,
}

let myFirstLanguage = Language.Korean;
let mySecondLanguage = Language.English;

console.log(myFirstLanguage); // --> 0
console.log(mySecondLanguage); // --> 1

console.log(Language[1]); // --> English
console.log(Language[0]); // --> Korean

"결과적으로 숫자 값을 받는 열거형은 전체 열거형의 안전성을 해칠 수 있다... 열거형을 안전하게 사용하는 방법은 까다로우므로 열거형 자체를 멀리 할 것을 권한다." -p.51


3-3. 스터디 노트

3-3-1. enum에 관하여

  • 상수를 그룹화하는 개념이라, 다른 언어에서는 적극 활용된다
  • 타입스크립트의 경우 자바스크립트로 컴파일 되게 되는데, 이때 가장 좋은 경우는 타입스크립트에서 타입만 제거되고 깔끔한 바닐라 자바스크립트만 남는게 좋다. 이를 tree shaking이라 하는데, enum의 경우 tree shaking해도 떨어지지 않는다. 즉 컴파일된 자바스크립트 코드에 흔적이 남게 된다.
// 타입스크립트에서 enum 사용

enum languages {
    Korean,
    Japanese,
    English,
}
// 자바스크립트로 컴파일 된 후

var languages;
(function (languages) {
    languages[languages["Korean"] = 0] = "Korean";
    languages[languages["Japanese"] = 1] = "Japanese";
    languages[languages["English"] = 2] = "English";
})(languages || (languages = {}));
  • 큰 프로젝트일 수록 tree shaking 안되는 것이 매우 큰 단점이다. 당연히 용량적인 문제도 있지만, 자바스크립트로 컴파일된 내용끼리 버그를 발생시킬 수도 있다.
  • 또한 enum은 merge된다.
  • 대안은 string literal type을 사용거나, as const를 사용하는 것이다