Literal Types(리터럴 타입)
Literal Types(리터럴 타입)은 특정한 값 자체를 타입으로 사용하는 것을 말한다. 타입스크립트에서는 문자열, 숫자, 불리언 값 등의 Literal Types을 지원하며, 이를 통해 값을 정확하게 지정하여 유효성 검사나 코드의 가독성을 높일 수 있다.
예를 들어, 아래와 같이 문자열 'hello'를 타입으로 지정할 수 있다.
let greeting: 'hello';
greeting = 'hello'; // OK
greeting = 'hi'; // 에러: Type '"hi"' is not assignable to type '"hello"'
위 예시에서 greeting 변수는 문자열 'hello'를 타입으로 지정되어 있으므로, 다른 값인 'hi'를 할당하면 컴파일러에서 에러를 발생시킨다.
Literal Types은 문자열 뿐만 아니라 숫자, 불리언, null, undefined 등 다양한 값들도 지정할 수 있다. 예를 들어, 아래와 같이 숫자 1을 타입으로 지정할 수 있다.
let value: 1;
value = 1; // OK
value = 2; // 에러: Type '2' is not assignable to type '1'
Literal Types은 보통 다른 타입과 함께 유니온 타입으로 사용되어, 특정 값들 중에서만 가능한 타입을 만드는 데 유용하게 사용된다.
예를 들어, 아래와 같이 문자열 'success' 또는 'error' 중 하나만 가능한 타입을 만들 수 있다.
type Result = 'success' | 'error';
function handleResult(result: Result) {
// ...
}
handleResult('success'); // OK
handleResult('error'); // OK
handleResult('failed'); // 에러: Type '"failed"' is not assignable to type 'Result'
인라인(Inline)과 타입 별칭(Type alias)으로 선언하기
Literal Types은 인라인으로 선언할 수도 있고, 타입 별칭(alias)으로 선언해서 재사용할 수도 있다.
// 인라인으로 선언하는 방법
let a: 'foo' = 'foo';
let b: 42 = 42;
// 타입 별칭(alias)을 사용하는 방법
type LogLevel = 'debug' | 'info' | 'warn' | 'error';
let logLevel: LogLevel = 'debug';
function logMessage(level: LogLevel, message: string) {
console.log(`[${level}] ${message}`);
}
logMessage('debug', 'Debug message'); // OK
logMessage('warn', 'Warning message'); // OK
logMessage('trace', 'Trace message'); // 에러: Argument of type '"trace"' is not assignable to parameter of type 'LogLevel'
조건부 타입(Conditional Types)과 함께 사용하기
Literal Types은 조건부 타입(Conditional Types)과 함께 사용되어, 타입 시스템에서 유용한 패턴을 만드는 데 활용된다.
예를 들어, 아래와 같이 Literal Types과 조건부 타입을 사용해서 입력 타입에 따라 다른 타입을 반환하는 함수를 만들 수 있다.
type IsString<T> = T extends string ? true : false;
function isString<T>(arg: T): IsString<T> {
return typeof arg === 'string' ? true : false;
}
console.log(isString('hello')); // true
console.log(isString(42)); // false
위 예시에서 IsString 타입은 T 타입이 string 타입일 때는 true를, 그렇지 않을 때는 false를 반환하는 타입이다. isString 함수는 입력값의 타입이 string인지 여부를 판별해서 IsString 타입의 값을 반환한다.
컴파일러는 isString('hello') 호출에서 T가 string 타입으로 지정될 것이므로, IsString<'hello'> 타입은 true가 된다. 반면 isString(42) 호출에서 T가 number 타입으로 지정될 것이므로, IsString<42> 타입은 false가 된다.
이와 같이 Literal Types과 조건부 타입을 결합해서 제네릭 타입의 입력값에 따라 다른 동작을 하는 함수를 만들 수 있다.
타입 가드(Type Guard)와함께 사용하기
Literal Types은 타입 가드(Type Guard)와 함께 사용해서 런타임에서 타입 안정성을 높이는 데도 유용하다.
예를 들어, 아래와 같이 문자열 또는 숫자 타입일 때만 값을 출력하는 함수를 만들 수 있다.
function printValue(value: string | number) {
if (typeof value === 'string') {
console.log(value.toUpperCase());
} else if (typeof value === 'number') {
console.log(value.toFixed(2));
}
}
printValue('hello'); // "HELLO"
printValue(3.141592); // 3.14
printValue(true); // 에러: Argument of type 'true' is not assignable to parameter of type 'string | number'
위 예시에서 printValue 함수는 입력값이 문자열이면 대문자로 변환하고, 숫자이면 소수점 아래 두 자리까지 출력한다. typeof 연산자를 사용해서 입력값의 타입을 확인하고, 해당 타입에 맞게 동작한다.
이와 같은 방식으로 Literal Types을 사용하면 런타임에서 타입 에러를 더 잘 잡아내고, 코드 안정성을 높일 수 있다.
열거형(Enumeration)와 함께 사용하기
Literal Types은 열거형(Enumeration)과 함께 사용해서 일정한 값의 집합을 표현하는 데도 유용하다. 열거형은 여러 개의 상수 값을 하나의 이름으로 묶어서 사용할 수 있는 타입이다. 이때, 열거형의 각 값은 Literal Types으로 지정된다.
enum Direction {
Up = 'UP',
Down = 'DOWN',
Left = 'LEFT',
Right = 'RIGHT'
}
let direction: Direction = Direction.Up;
console.log(direction); // "UP"
console.log(Direction.Down); // "DOWN"
위 예시에서 Direction 열거형은 Up, Down, Left, Right 값 중 하나를 가지는 타입이다. 각 값은 문자열 Literal Types으로 지정되어 있다. direction 변수는 Direction 타입으로 선언되어 있으며, Direction.Up 값으로 초기화됐다.
Literal Types은 타입스크립트에서 강력한 타입 시스템을 구성하는 데 중요한 역할을 한다.Literal Types은 코드 안정성을 높이고, 타입에 대한 명확한 의도를 표현하는 데 유용하며, 다른 기능과 결합해서 더 강력한 타입 패턴을 만드는 데도 활용된다.
Template Literal Types(템플릿 리터럴 타입)
타입스크립트(TypeScript) 4.1부터 도입된 Template Literal Types(템플릿 리터럴 타입)은 문자열 템플릿 리터럴을 사용하여 타입을 정의하는 기능이다.
템플릿 리터럴을 이용하면 문자열을 변수나 표현식과 함께 사용할 수 있다. 예를 들어, 다음과 같은 문자열을 만들 수 있다.
const name = 'John';
const message = `Hello, ${name}!`;
템플릿 리터럴 타입은 이러한 문자열을 타입으로 정의할 수 있다. 예를 들어, 다음과 같은 템플릿 리터럴 타입을 만들 수 있다.
type Greeting<T extends string> = `Hello, ${T}!`;
이제 Greeting 타입은 문자열 'Hello, John!'을 나타내는 것이 아니라, 'Hello, ${T}!'와 같은 형태를 가진 타입이다.
이 타입은 문자열 보간 기능을 사용하여 다양한 타입을 만들 수 있다. 예를 들어, 다음과 같은 타입을 만들 수 있다.
type GreetingWithNumber<T extends number> = `${T} bottles of beer on the wall`;
이제 GreetingWithNumber 타입은 1 bottles of beer on the wall, 2 bottles of beer on the wall과 같은 형태를 가진 타입이다.
또한, 템플릿 리터럴 타입은 보간 기능을 사용하여 변수나 표현식에 의존하는 타입을 만들 수 있다. 예를 들어, 다음과 같은 템플릿 리터럴 타입을 만들 수 있다.
type FullName<First extends string, Last extends string> = `${First} ${Last}`;
이제 FullName 타입은 이름과 성을 나타내는 문자열 타입이다. First와 Last는 타입 매개변수로, 각각 이름과 성을 나타내는 문자열 타입을 받는다. 이 타입을 사용하여 다음과 같이 변수를 선언할 수 있다.
const name: FullName<'John', 'Doe'> = 'John Doe';
이제 name 변수는 문자열 'John Doe'를 값으로 가지며, 이 값은 FullName<'John', 'Doe'> 타입과 일치하다.
템플릿 리터럴 타입은 타입스크립트에서 사용되는 다양한 기능과 함께 사용될 수 있다.
조건부 타입과 함께 사용하기
조건부 타입을 사용하면 타입 매개변수에 따라 다른 타입을 반환할 수 있다. 이때 템플릿 리터럴 타입을 사용하면 매우 유용하다.
type Wrap<T, IsArray extends boolean> = IsArray extends true ? T[] : T;
이제 Wrap 타입은 T 타입을 IsArray 타입 매개변수에 따라 배열로 래핑하는 타입이다. 이 타입을 사용하여 다음과 같이 변수를 선언할 수 있다.
const x: Wrap<number, true> = [1, 2, 3];
const y: Wrap<string, false> = 'hello';
이제 x 변수는 number[] 타입이 되며, [1, 2, 3]와 같은 배열을 값으로 가질 수 있다. y 변수는 string 타입이 되며, 'hello'와 같은 문자열을 값으로 가질 수 있다.
조건부 타입을 사용하여 조건부 분배하기
템플릿 리터럴 타입은 조건부 타입을 사용하여 분배 법칙(distribution law)을 적용할 수 있는 방법을 제공한다.
type PropType<T, K extends keyof T> = T[K];
type SpreadProp<T, U> = { [K in keyof U]: K extends keyof T ? PropType<T, K> : U[K] } & T;
이제 PropType 타입은 객체 T의 속성 K의 타입을 나타내는 타입이다.
SpreadProp 타입은 객체 U의 속성을 객체 T에 추가하는 타입이다. 이때 SpreadProp 타입은 T와 U의 속성이 중첩되지 않도록 조건부 타입을 사용하여 구현된다.
이제 SpreadProp 타입을 사용하여 다음과 같이 객체를 확장할 수 있다.
interface Person {
name: string;
age: number;
}
interface AdditionalInfo {
gender: string;
address: string;
}
type ExtendedPerson = SpreadProp<Person, AdditionalInfo>;
이제 ExtendedPerson 타입은 Person과 AdditionalInfo의 속성을 모두 가지는 타입이다.
이 타입은 { name: string; age: number; gender: string; address: string; }와 같은 형태를 가질 수 있다.
문자열 상수를 타입으로 사용하기
템플릿 리터럴 타입은 문자열 상수를 타입으로 사용할 수 있는 방법을 제공한다.
type LogLevel = 'debug' | 'info' | 'warn' | 'error';
이제 LogLevel 타입은 debug, info, warn, error 중 하나의 문자열 값을 가지는 타입이다. 이 타입을 사용하여 함수를 작성할 수 있다.
function log(level: LogLevel, message: string) {
console[level](message);
}
log('debug', 'debug message');
log('info', 'info message');
log('warn', 'warning message');
log('error', 'error message');
이제 log 함수는 LogLevel 타입에 해당하는 문자열 상수를 첫 번째 매개변수로 받으며, 해당 문자열 값에 따라 적절한 로그 함수를 호출한다.
문자열 조작하기
템플릿 리터럴 타입은 문자열을 조작할 수 있는 방법을 제공한다.
type ToUpperCase<S extends string> = S extends `${infer F}${infer R}` ? `${Uppercase<F>}${R}` : S;
이제 ToUpperCase 타입은 문자열 S를 대문자로 변환하는 타입이다. 이 타입을 사용하여 다음과 같이 변수를 선언할 수 있다.
type Foo = ToUpperCase<'hello'>; // 'HELLO'
type Bar = ToUpperCase<'world'>; // 'WORLD'
이제 Foo 변수는 'hello' 문자열을 대문자로 변환한 'HELLO' 문자열 값을 가진다.
Bar 변수는 'world' 문자열을 대문자로 변환한 'WORLD' 문자열 값을 가진다. 이렇게 템플릿 리터럴 타입을 사용하면 문자열을 쉽게 조작할 수 있다.
Non-nullable Types(논널러블 타입)
Non-nullable Types(논널러블 타입)는 타입스크립트 2.0에서 도입된 기능으로, 변수나 매개변수가 null 또는 undefined를 허용하지 않는 타입을 지정하는 것을 말한다.
기존에는 모든 변수가 암묵적으로 null 또는 undefined를 허용하도록 지정되어 있었기 때문에, 이러한 값을 처리하지 않고 무시할 경우 버그의 원인이 될 수 있었습니다. 하지만 Non-nullable Types을 사용하면 해당 변수가 null 또는 undefined를 허용하지 않도록 강제할 수 있기 때문에, 이러한 버그를 사전에 방지할 수 있다.
논널러블 타입을 사용하려면, 변수나 매개변수의 타입 뒤에 물음표(?)를 제거하면 된다. 예를 들어, string? 대신 string을 사용하면 해당 변수가 null 또는 undefined를 허용하지 않도록 지정할 수 있다.
다음은 논널러블 타입을 사용하여 null 또는 undefined를 허용하지 않도록 지정한 예시다.
function printName(name: string) {
console.log(name);
}
printName("John"); // "John"
printName(null); // Error: Argument of type 'null' is not assignable to parameter of type 'string'.
printName(undefined); // Error: Argument of type 'undefined' is not assignable to parameter of type 'string'.
위 예시에서 printName 함수의 매개변수인 name은 논널러블 타입인 string으로 지정되어 있기 때문에, null 또는 undefined를 전달할 경우 컴파일 오류가 발생한다.
논널러블 타입은 코드의 안정성과 가독성을 높여주기 때문에, 타입스크립트에서 권장되는 기능 중 하나다.
또한, Non-nullable Types을 사용하면 특히 함수의 매개변수나 반환값을 처리할 때 유용하다.
function getFullName(firstName: string, lastName?: string) {
if (lastName) {
return `${firstName} ${lastName}`;
} else {
return firstName;
}
}
위 함수는 firstName 매개변수는 반드시 전달되어야 하지만, lastName 매개변수는 선택적으로 전달할 수 있다. 하지만 Non-nullable Types을 사용하면 lastName 매개변수를 undefined로 처리할 수 없도록 지정할 수 있다.
function getFullName(firstName: string, lastName: string) {
return `${firstName} ${lastName}`;
}
위 코드에서 lastName 매개변수는 Non-nullable Types인 string으로 지정되어 있기 때문에, 함수를 호출할 때 반드시 값을 전달해야 한다. 만약 값을 전달하지 않을 경우 컴파일 오류가 발생한다.
또한 Non-nullable Types을 사용하면 코드의 가독성도 높아진다. 코드에서 매개변수나 변수의 타입이 Non-nullable Types인 경우, 해당 값이 반드시 존재해야 한다는 것을 명확하게 알 수 있기 때문이다.
하지만 Non-nullable Types을 사용할 때 주의해야 할 점도 있다.
사용 시, 주의사항
- 어떤 경우에는 null 또는 undefined가 필요할 수 있다.
- 이 경우에는 유니온 타입을 사용하여 null 또는 undefined를 포함하는 타입을 지정해주어야 한다. - 어떤 경우에는 null 또는 undefined가 전달될 가능성이 있는 함수를 다룰 때 주의해야 한다.
- 이 경우에는 매개변수의 타입을 Non-nullable Types이 아닌 유니온타입으로 지정하거나, 함수 내부에서 null 체크를 수행하여 예외 처리를 해주어야 한다. - JavaScript에서의 동작과 일치시키기 위해 몇 가지 예외 사항이 있다.
- 예를 들어, Non-nullable Types으로 지정된 객체의 프로퍼티는 null 또는 undefined를 포함할 수 있다. 이 경우에는 Non-nullable Types을 적용할 수 없으므로, 유니온 타입을 사용하여 해당 객체의 프로퍼티가 null 또는 undefined를 포함할 수 있는 타입을 지정해주어야 한다. - Non-nullable Types은 코드의 안정성과 가독성을 높여주기 때문에, 타입스크립트에서 권장되는 기능 중 하나이다. 하지만 Non-nullable Types을 과도하게 사용하면, 코드의 유연성이 떨어지게 된다. 따라서 Non-nullable Types을 사용할 때에는 상황에 맞게 적절히 사용하는 것이 중요하다.
Indexed Access Types(인덱싱 접근 타입)
Indexed Access Types(인덱싱 접근 타입)는 특정 객체 혹은 인터페이스 타입에서 특정 속성의 타입을 동적으로 가져오는 기능이다.
interface Person {
name: string;
age: number;
gender: string;
}
여기서 Person 인터페이스에서 name 속성의 타입을 가져오기 위해서는 다음과 같은 방식으로 타입을 지정할 수 있다.
type NameType = Person['name']; // string
위 예시에서, Person['name']은 string이라는 값을 반환한다. 이것은 Person 인터페이스의 name 속성의 타입을 동적으로 가져오는 것이다.
제네릭 타입(Generic Types)과 함께 사용하기
Indexed Access Types은 일반적으로 제네릭 타입과 함께 사용되어, 동적으로 속성 타입을 가져오는 유연한 방법을 제공한다.
type ValueOf<T> = T[keyof T];
위의 ValueOf 타입은 입력된 객체 T의 모든 속성 중 하나의 값에 해당하는 타입을 반환한다.
예를 들어, Person 인터페이스의 name 속성의 타입을 가져오기 위해서는 다음과 같이 사용할 수 있다.
type NameType = ValueOf<Person>; // string
이렇게 하면 NameType 타입은 Person 인터페이스의 name 속성의 타입인 string으로 지정된다.
복잡한 타입에서 사용하기
Indexed Access Types은 더욱 복잡한 타입을 다룰 때 유용하다.
interface User {
id: number;
name: string;
email: string;
phone: string;
address: {
street: string;
city: string;
state: string;
zip: string;
};
}
여기서 User 인터페이스에서 address 속성의 street와 city 속성만 추출하여 새로운 인터페이스를 생성하고 싶다면, 다음과 같은 방식으로 타입을 지정할 수 있다.
type AddressFields = Pick<User['address'], 'street' | 'city'>;
위 예시에서 Pick은 객체나 인터페이스에서 필요한 속성만 선택하여 새로운 객체나 인터페이스를 생성하는 함수다.
User['address']는 User 인터페이스에서 address 속성의 타입을 반환하며, 'street' | 'city'는 User['address']에서 선택하고자 하는 속성의 이름을 유니온 타입으로 지정한 것이다.
이렇게 함으로써 AddressFields 타입은 다음과 같이 정의된다.
interface AddressFields {
street: string;
city: string;
}
문자열 타입을 인덱스로 사용하기
Indexed Access Types을 사용하면 객체 타입에서 인덱스로 문자열 또는 숫자 타입을 사용할 수 있다.
interface User {
id: number;
name: string;
email: string;
phone: string;
address: {
street: string;
city: string;
state: string;
zip: string;
};
}
User 인터페이스에서 문자열 인덱스 타입을 사용하여 동적으로 속성 타입을 가져오는 방법은 다음과 같다.
type UserAddressFields = User['address'][keyof User['address']];
위 예시에서 keyof User['address']는 User['address'] 속성에서 가능한 모든 속성 키를 유니온 타입으로 반환하며, 이를 인덱스 타입으로 사용하여 User['address'] 속성의 모든 속성 타입을 유니온 타입으로 추출한다.
위의 UserAddressFields 타입은 다음과 같이 정의된다.
type UserAddressFields = string;