타입스크립트에서 타입의 모든 것

Jin·2022년 3월 1일
0

Typescript

목록 보기
2/4

타입이란 값과 이 값으로 할 수 있는 일의 집합입니다.

  • Boolean 타입은 모든 bool (참과 거짓 중 하나)과 bool에 수행할 수 있는 모든 연산 (||, &&, ! 등)의 집합입니다.
  • number 타입은 모든 숫자와 숫자에 적용할 수 있는 모든 연산 (+, -, *, /, %, ||, &&, ? 등)의 집합입니다.
  • string 타입은 모든 문자열과 문자열에 수행할 수 있는 모든 연산 (+, ||, && 등)과 문자열에 호출할 수 있는 모든 메서드 (.concat, .toUpperCase 등)의 집합입니다.

어떤 값이 T 타입이라면, 이 값을 가지고 어떤 일을 할 수 있고 어떤 일을 할 수 없는지도 알 수 있습니다. 여기서 중요한 것은 타입 검사기를 이용해 유효하지 않은 동작이 실행되는 일을 예방하는 것입니다.

function squareOf(n: number) { 
	return n * n; 
}

squareOf(2); // 4 
squareOf('z'); // error

이 squareOf라는 함수는 숫자를 파라미터로 받아야지만 명확히 동작하며, 숫자가 아닌 다른 타입을 전달하면 유효하지 않은 작업을 수행하게 됩니다. 따라서, 매개변수의 타입을 명시하여야 합니다. 타입 어노테이션이 없으면 squareOf의 매개변수에 제한이 없으므로 아무 타입이나 인수로 전달할 수 있게 됩니다. 자바스크립트 (이하 JS)는 문자열이 인수로 들어와도 에러 없이 NaN을 반환할 것이며, 타입스크립트 (이하 TS)는 곧바로 에러를 발생시킬 것입니다.

여기서 우리는 TS가 특정 타입만 와야 할 때 이를 명시할 수 있는 언어라는 것을 알 수 있습니다.


타입의 종류

any

any는 타입들의 대부입니다. any로 뭐든지 할 수 있지만 꼭 필요한 상황이 아니라면 사용하지 않는 것이 좋습니다. TS에서는 컴파일 타임에 모두가 타입이 있는 상황이 베스트이므로 프로그래머와 TS 둘 다 타입을 알 수 없는 상황에서는 기본 타입인 any라고 가정합니다. any는 최후의 보루로, 가급적 사용하지 않는 것이 좋고 TS의 취지에도 부합됩니다.
앞서 말했다시피, 타입은 값과 값으로 수행할 수 있는 작업의 집합입니다. any는 모든 값과 모든 값으로 수행할 수 있는 작업의 집합을 의미하므로 모든 것을 할 수 있습니다. 이는 곧 값이 JS의 값처럼 동작하면서 TS의 타입 검사기라는 마법이 더 이상 작동하지 않게 합니다.
굳이 any를 사용해야 하는 상황이라면 명시적으로 선언하여야 합니다.

let a: any = 666; // any 
let b: any = ['danger']; // any 
let c = a + b; // any

unknown

결론부터 먼저 말하자면, 타입을 미리 알 수 없는 어떤 값이 있을 때는 any 대신 unknown을 사용하는 것이 좋습니다. any처럼 unknown도 모든 값을 대표하지만, unknown의 타입을 검사해 정제하기 전까지는 TS가 unknown 타입의 값을 사용할 수 없게 강제하기 때문입니다.
unknown은 비교 연산 (==, ===, ||, &&, ?)과 반전(!) 연산을 지원하고 typeof나 instanceof 메서드로 조건을 걸 수 있습니다.

let a: unknown = 30; // unknown 
let b = a === 123; // boolean 
let c = a + 10; // error 

if (typeof a === 'number') { 
	let d = a + 10; // number 
}

변수 c는 unknown 타입의 값을 연산에 사용하려 하므로 에러가 발생하게 됩니다. 하지만 만약 해당 값이 특정 타입인 것이 증명됐다면, 그 후에는 정상적으로 사용할 수 있게 됩니다.

boolean

boolean 타입은 true와 false 두 개의 값을 가집니다. 이 타입으로는 비교 연산과 반전 연산을 할 수 있을 뿐 그 외의 지원을 지원하지는 않습니다.

let a = true; // boolean 
let b = false; // boolean 
const c = true; // true 
let d: boolean = true; // boolean 
let e: true = true; // true 
let f: true = false; // error

위의 코드를 통해 어떤 값이 boolean인지 TS에 알려줄 수 있는 여러 방법을 확인할 수 있습니다.

  • 어떤 값이 boolean인지 TS가 추론하게 한다. (a, b)
  • 어떤 값이 특정 boolean인지 TS가 추론하게 한다. (c)
  • 값이 boolean임을 명시적으로 TS에 알린다. (d)
  • 값이 특정 boolean임을 명시적으로 TS에 알린다. (e, f)

실제 프로그래밍에서는 1, 2번째 방법을 주로 사용합니다. c와 f을 선언할 때에 boolean 타입이 가질 수 있는 값 중 특정한 하나의 값으로 한정하였는데 이 기능을 타입 리터럴이라고 합니다.

타입 리터럴은 오직 하나의 값을 나타내는 타입입니다.
c를 선언할 때에는 const를 사용했으므로 TS는 그 변수의 값이 절대 변하지 않으리라는 사실을 알게 되어 해당 변수가 가질 수 있는 가장 좁은 타입으로 추론합니다.

타입 리터럴은 모든 곳에서 일어날 수 있는 실수를 방지해 안전성을 추가로 확보해주는 강력하는 언어 기능입니다.

number

number 타입은 모든 숫자 (정수, 소수, 양수, 음수, Infinity, NaN 등)의 집합입니다. number 타입에는 +, -, %, <, > 등의 숫자 관련 연산을 수행할 수 있습니다.

let a = 1234; // number 
let b = Infinity * 0.1; // number 
const c = 1234; // 1234 
let d = a < b; // boolean 
let e: number = 100; // number 
let f: 12.34 = 12.34; // 12.34 
let g: 23.45 = 12.34 // error 
let h = 1000_000; // 1000000과 같음

위의 예제에서 볼 수 있듯이 number 타입을 지정할 수 있는 방법은 다음과 같습니다.

  • TS가 값이 number임을 추론하게 한다. (a, b)
  • const를 이용해 TS가 값이 특정 number임을 추론하게 한다. (c)
  • 값이 number임을 명시적으로 TS에 알린다. (e)
  • TS에 값이 특정 number임을 명시적으로 알린다. (f, g)

보통은 boolean처럼 대개 TS가 number 타입을 추론하도록 만듭니다.

숫자 대입 도중 _을 삽입하게 되면 분리자 역할을 수행하며 숫자를 읽기 쉽게 만들 수 있습니다.

bigint

bigint는 JS와 TS에 새로 추가된 타입으로, 이를 이용하면 라운딩 관련 에러 걱정 없이 큰 정수를 처리할 수 있습니다. number는 2의 53승까지의 정수를 표현할 수 있지만 bigint를 이용하면 이보다 큰 수도 표현할 수 있게 됩니다.
bigint 타입은 +, -, *, /, <, > 등의 연산을 지원하고 다음처럼 사용됩니다.

let a = 1234n; // bigint 
const b = 5678n; // 5678n 
let c = a + b; // bigint 
let d = a < 1235; // boolean <-- true 
let e = 88.5n; // error <-- bigint 리터럴은 반드시 정수여야 함. 
let f: bigint = 100n; // bigint 
let g: 100n = 100n; 
let h: bigint = 100; // error <-- 100의 타입은 bigint 타입에 할당할 수 없음.

이처럼 여러 형태로 bigint를 선언할 수 있지만 가능하면 TS가 bigint의 타입을 추론하게 만드는 것이 좋습니다.

string

string은 모든 문자열의 집합으로 +, .slice 등의 연산을 수행할 수 있습니다.

let a = 'hello'; // string 
const c = '!'; // '!' 
let d = a + '' + c; // string 
let e: string = 'zoom'; // string 
let f: 'john' = 'john'; // 'john' 
let g: 'john' = 'zoe'; // error

string 타입도 boolean과 number처럼 네 가지 방법으로 선언할 수 있으며 가능하다면 TS가 string 타입을 추론하도록 두는 것이 좋습니다.

symbol

symbol은 ES2015에 새로 추가된 기능입니다. 실무에서는 symbol을 자주 사용하지는 않는다고 하며 객체와 맵에서 문자열 키를 대신하는 용도로 사용합니다. symbol 키를 사용하면 사람들이 잘 알려진 키만 사용하도록 강제할 수 있으므로 키를 잘못 설정하는 실수를 방지합니다.

let a = Symbol('a'); // symbol 
let b: symbol = Symbol('b'); // symbol 
let c = a === b; // boolean <-- false 
let d = a + 'x'; // error 
const e = Symbol('e'); // typeof e 
const f: unique symbol = Symbol('f'); // typeof f 
let g: unique symbol = Symbol('f'); // error <-- unique symbol 타입은 반드시 const여야 함 
let h = e === e; // boolean 
let i = e === f; // error <-- unique symbol 타입은 겹치는 일이 없으므로 이 비교문의 결과는 항상 false

symbol은 고유하여 다른 symbol과 비교하였을 때 같지 않다고 판단됩니다. 심지어는 같은 이름으로 다른 symbol을 만들어서 비교해도 그렇습니다.
위의 코드에서 볼 수 있듯이 unique symbol을 만드는 경우는 다음과 같습니다.

  • 새 symbol을 선언하고 const 변수에 할당하면 TS가 unique symbol 타입으로 추론합니다. 코드 편집기에서는 unique symbol이 아니라 'typeof 변수명' 형태로 보여줄 것입니다.
  • const 변수의 타입을 unique symbol로 명시적으로 정의할 수 있습니다.
  • unique symbol은 자신과 항상 같습니다.
  • TS는 컴파일 타임에 unique symbol이 다른 unique symbol과 같지 않을 것이라는 사실을 알 수 있습니다.

unique symbol도 결국 1, true, 'hello' 등의 다른 리터럴 타입과 마찬가지로 특정 symbol을 나타내는 타입입니다.

객체

TS의 객체 타입은 객체의 형태를 정의합니다. TS에서 객체를 서술하는 데 타입을 이용하는 방식은 여러 가지입니다.

1. 값을 object로 선언하는 것

let a: object = { b: 'x' }

b에 접근하려고 하면 에러가 발생합니다.
a의 타입을 객체라고 명시하지 않고 TS가 추론하도록 하면 에러가 발생하지 않습니다.

2. TS가 객체의 형태를 추론하게 하거나 중괄호 안에서 명시적으로 타입을 묘사한다.

const a: {b: number} = { b: 12 }

이제 b에 정상적으로 접근해도 문제가 없습니다. 그리고 여전히 b의 타입은 12가 아닌 number입니다. 지금까지 살펴본 boolean, number, string 같은 기본 타입과 달리 객체는 const로 선언해도 TS는 더 좁은 타입으로 추론하지 않습니다. JS 객체의 값은 바뀔 수 있으며, TS도 여러분이 객체를 만든 후 필드 값을 바꾸려 할 수 있다는 사실을 알고 있기 때문입니다.
만약, 명시한 대로 프로퍼티를 선언하지 않으면 곧바로 에러가 발생합니다.
기본적으로, TS는 객체 프로퍼티에 엄격한 편입니다. 예를 들어, 객체에 number 타입의 b라는 프로퍼티가 있어야 한다고 정의하면 TS는 오직 b만 기대합니다. b가 없거나 다른 추가 프로퍼티가 있으면 에러를 발생시킵니다.
하지만, 프로퍼티를 선택형으로 추가할 수도 있습니다.

let a: { 
  b: number 
  c?: string 
  [key: number]: boolean 
}

위의 코드로 알 수 있는 점은 3가지입니다.

  • a는 number 타입의 프로퍼티 b를 포함한다.
  • a는 string 타입의 프로퍼티 c를 포함할 수도 있다.
  • a는 boolean 타입의 값을 갖는 number 타입의 프로퍼티를 여러 개 포함할 수 있다.

여기서, [key: T]: U 같은 문법을 인덱스 시그니처라고 부르며 TS에 어떤 객체가 여러 키를 가질 수 있음을 알려줍니다. 인덱스 시그니처에서 기억해야 할 규칙이 하나 있습니다. 인덱스 시그니처의 키 (T)는 반드시 number나 string 타입에 할당할 수 있는 타입이어야 합니다. 인덱스 시그니처의 키 이름은 원하는 이름을 가져다 바꿔도 됩니다. 즉, [haha: number]: boolean처럼 선언하여도 됩니다.
객체 타입을 정의할 때 필요하다면 readonly를 사용하여 특정 필드를 읽기 전용으로 정의할 수 있습니다.

let user: { 
	readonly firstname: string 
} = { 
	firstname: 'hello' 
}; 

user.firstname = 'hi'; // error

읽기 전용 프로퍼티이므로 값을 변경하려고 하면 에러가 발생합니다.

그리고, let danger: {} 같이 빈 객체 타입의 객체는 가능한 한 피하는 것이 좋습니다. 모든 타입을 빈 객체 타입에 할당할 수 있으나, 사용하기 까다롭게 만들기 때문입니다.

TS에서 객체를 정의하는 방법은 다음처럼 네 가지로 요약할 수 있습니다.

  • 객체 리터럴이라 불리는 표기법 ({a: string}). 객체가 어떤 필드를 포함할 수 있는지 알고 있거나 객체의 모든 값이 같은 타입을 가질 때 사용합니다.
  • 빈 객체 리터럴 표기법 ({}). 이 방법은 사용하지 않는 것이 좋습니다.
  • object 타입. 어떤 필드를 가지고 있는지는 관심 없고 그저 객체가 필요할 때 사용합니다.
  • Object 타입. 사용하지 않는 것이 좋습니다.

타입 별칭

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

let age: Age = 27; 
let driver: Person = { 
	name: 'jimmy', 
	age: age 
}

위처럼 변수를 선언하듯 타입 별칭을 선언하여 타입을 가리키게 할 수 있습니다.
TS는 별칭을 추론하지는 않으므로 반드시 별칭의 타입은 명시적으로 정의하여야 합니다.
let과 const처럼 타입 별칭도 블록 스코프로 적용되므로 내부에 정의한 타입 별칭이 외부의 정의를 덮어쓰게 됩니다.

관행적으로, 타입 별칭의 이름은 대문자로 시작합니다.

|과 &

|은 합집합, &은 교집합을 의미합니다.

type Cat = {
	name: string, 
	purrs: boolean
} 
type Dog = {
	name: string, 
	barks: boolean, 
	wags: boolean
} 
type both = Cat | Dog 
type CatDog = Cat & Dog

both에서 알 수 있는 것은 문자열 타입의 name 프로퍼티가 있다는 것입니다. both에는 Cat 또는 Dog 아니면 둘 다 할당할 수 있습니다.
CatDog은 Cat과 Dog의 모든 프로퍼티들이 전부 선언되어야 합니다.

실무에서는 대개 &보다는 |을 사용한다고 합니다.

type Returns = string | null 
function f(a: string, b: number) { 
	return a || b; 
}

Returns는 string 타입이나 null 타입일 수 있다는 것을 알려주고 f 함수는 반환 값의 타입이 string 혹은 number라는 것을 알려줍니다.

배열

JS처럼 TS 배열도 concat, push, slice 등의 메서드를 지원하는 특별한 객체입니다.

let a = [1, 2, 3]; // number[] 
let b = ['a', 'b']; // string[] 
let c: string[] = ['a']; // string[] 
let d = [1, 'a']; // (string | number)[] 
const e = [2, 'b']; // (string | number)[] 

let f = ['red']; 
f.push('blue'); 
f.push(true); // error 

let g = []; // any[] 
g.push(1); 
g.push('a'); 
g.push(true); 

let h: number[] = []; 
h.push(1); 
h.push('red'); // error

위의 예제를 보면 c와 h를 제외한 모든 변수의 타입은 묵시적으로 정의합니다.

대개, 배열은 같은 타입의 값으로 이루어집니다. 즉, 배열의 모든 항목이 같은 타입을 갖도록 설계하려고 노력하여야 합니다. 그래야 안전한 배열이기 때문입니다.

객체와 마찬가지로 배열을 const로 만들어도 TS는 타입을 더 좁게 추론하지 않습니다.

g는 특별한 상황으로, 빈 배열로 초기화하면 TS는 배열의 요소 타입을 알 수 없으므로 any일 것으로 추측합니다. 만약, 빈 배열로 초기화해서 number 타입의 요소를 추가하면 TS는 주어진 정보를 이용해 배열의 타입을 추론합니다. 배열이 정의된 영역을 벗어나면 최종 타입을 확정합니다.

function buildArr() { 
	let a = []; // any[] 
	a.push(1); // number[] 
	a.push('x'); // (string | number)[] 
	return a; 
}

위의 함수에서 반환된 배열은 호출부에서 절대로 타입을 변경할 수 없습니다. 즉, buildArr()로 반환된 배열의 타입은 호출부 입장에서는 항상 (string | number)[] 입니다.

TS에서는 읽기 전용 배열도 제공합니다.

let as: readonly number[] = [1, 2, 3]; 
as.push(4); // error

읽기 전용 배열은 불변하므로 코드를 쉽게 이해할 수 있다는 장점이 있지만 결국 JS 배열로 구현한 것입니다. 즉, spread 오퍼레이터나 .slice 등으로 배열을 조금만 바꿔도 우선 원래 배열을 복사해야 하므로, 주의하지 않으면 프로그램의 성능이 느려질 수 있습니다. 작은 배열에서는 이런 오버헤드가 사소하지만 큰 배열에서는 눈에 띄게 큰 성능 저하를 일으킬 수 있기 때문입니다.

튜플

튜플은 배열의 서브타입입니다. 길이가 고정되었고, 각 인덱스의 타입이 알려진 배열의 일종입니다.

다른 타입과 달리 튜플은 선언할 때 타입을 명시해야 합니다.

let a: [number] = [1]; 

// [이름, 성, 나이] 튜플 
let b: [string, string, number?] = ['jimmy', 'pat', 27]; 
b = [12, 13, 14]; // error

선택형으로 추가할 수 있고 튜플이 하나의 객체이기 때문에 명시된 구조에 벗어나게 할당하면 에러가 발생합니다.

또한, 튜플이 최소 길이를 갖도록 지정할 때는 spread 오퍼레이터를 사용할 수 있습니다.

// 최소 한 개의 요소를 갖는 string 배열 
let friends: [string, ...string[]] = ['pat', 'jin', 'katie']; 

// 이형 배열 
let list: [number, boolean, ...string[]] = [1, true, 'jim', 'a'];

튜플은 이형 배열은 안전하게 관리할 수 있고 배열 타입의 길이도 조절합니다. 이런 기능을 잘 활용하면 순수 배열에 비해 안정성을 높일 수 있으므로 튜플 사용을 권장합니다.

null, undefined, void, never

JS는 null, undefined 두 가지 값으로 '값이 없음'을 표현합니다.
TS에서 undefined 값의 타입은 오직 undefined이고 null 값의 타입은 오직 null 뿐이라는 점에서 특별한 타입입니다.
두 타입의 의미는 엄밀히 말하면 다릅니다.

undefined는 '아직 정의하지 않았음'을 의미하는 반면 null은 '값이 없다'는 의미입니다.

TS는 null과 undefined 외에도 void와 never 타입을 제공하여 '값이 없음'의 특징을 좀 더 세밀하게 분류합니다.

void는 명시적으로 아무것도 반환하지 않는 함수의 반환 타입을 가리키며 never는 절대 반환하지 않는 함수 타입을 가리킵니다.

function a(x: number) { 
	return null; 
} 

function b() { 
	return undefined; 
} 

function c() { 
	let num = 2 + 2; 
} 

function d() { 
	throw Error(); 
} 

function e() { 
	while (true) { 
    	b(); 
    } 
}
  • a는 null을 반환합니다.
  • b는 undefined를 반환합니다.
  • c는 undefined를 반환하지만 명시적인 return 문을 사용하지 않았으므로 void를 반환한다고 할 수 있습니다.
  • d는 예외를 던지므로 절대 반환하지 않는 함수입니다. (never)
  • e는 무한 루프를 실행하므로 절대 반환하지 않는 함수입니다. (never)

unknown이 모든 타입의 상위 타입이라면 never는 모든 타입의 서브타입입니다. 즉, 모든 타입에 never를 할당할 수 있으며 never 값은 어디서든 안전하게 사용할 수 있습니다.

enum (열거형)

enum은 해당 타입으로 사용할 수 있는 값을 열거하는 기법입니다. 키를 값에 할당하는, 순서가 없는 자료구조이므로 키가 컴파일 타임에 고정된 객체라고 생각하면 쉽습니다. 따라서, TS는 키에 접근할 때 주어진 키가 실제 존재하는지 확인할 수 있습니다.

enum Language { 
	Korean, 
	English, 
	Russian, 
	Spanish 
}

위의 예제처럼 enum의 이름은 단수 명사로 하고, 첫 문자는 대문자로 하는 것이 관례입니다. 키도 앞 글자는 대문자로 표시합니다.
TS는 자동으로 enum의 각 멤버에 적절한 숫자를 추론해 할당하지만, 값을 명시적으로 설정할 수도 있습니다.

enum Language { 
	Korean = 0, 
	English = 1, 
	Russian = 2, 
	Spanish = 3 
} 

let rus = Language.Russian; 
console.log(rus); // 2 
let eng = Language['English']; 
console.log(eng); // 1

보통의 객체에서 값을 가져오는 것처럼 점 또는 괄호 표기법으로 값에 접근할 수 있습니다.

TS는 여러 enum 정의 중 한 가지 값만 추론할 수 있으므로 열거형을 분할할 때는 주의해야 하며, 각 열거형 멤버에 명시적으로 값을 할당하는 습관을 기르는 것이 좋습니다.

enum Language { 
	Korean = 0, 
	English = 1, 
	Russian = 2, 
	Spanish 
} 

let spa = Language['Spanish']; 
console.log(spa); // 3

만약, 모든 값을 정의하지 않으면 TS가 빠진 값을 추론합니다.

Spanish는 이전 값인 Russian에 할당된 값이 2이므로 +1인 3으로 추론합니다.

enum에는 문자열 값을 사용하거나 문자열과 숫자 값을 혼합할 수는 있지만 이는 불안정한 결과를 초래하므로 사용하지 않는 것이 좋습니다.

enum Language { 
	Korean = 0, 
	English = 1, 
	Russian = 2, 
	Spanish 
} 

console.log(Language[6]); // undefined

enum은 숫자 키의 접근을 허용합니다. 위의 경우에도 에러가 발생하여야 하지만 JS처럼 undefined를 반환하며 에러가 발생하지 않습니다.
이런 불안정성을 위해 const enum으로 선언하는 방법을 찾았지만 이마저도 완벽하지 않으므로 보통 문자열 값을 갖는 열거형을 사용합니다.

결론적으로는 열거형을 안전하게 사용하는 방법 자체가 까다로우므로 열거형 자체를 멀리 하는 것이 좋습니다. TS에는 enum을 대체할 수단이 많으므로 굳이 TS의 할당 규칙 때문에 생긴 운 나쁜 타입을 사용하여 안전성을 해칠 필요는 없기 때문입니다.

profile
배워서 공유하기

0개의 댓글