TypeScript
<타입스크립트 프로그래밍> 3장: 타입의 모든 것
sqsung
2023. 5. 26. 12:43
3장: 타입의 모든 것
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'.
- 단, 이렇게 하는 경우 값의 프로퍼티에 접근할 수 없다
- 사실
object
는any
보다 좁은 타입이다.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
타입은 "값이 없다"는 의미다- 타입스크립트는 추가로 "존재하지 않음"을 더 세밀하게 분류할 수 있도록
void
와never
타입도 제공한다 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
를 사용하는 것이다