Template Literal Type
기본 사용법
템플릿 리터럴 타입은 백틱(```)으로 둘러싸인 문자열 내에서 표현식을 사용할 수 있다. 예를 들어:
type World = 'world';
type Greeting = `hello ${World}`;Greeting 타입은 문자열 'hello world'의 타입이 된다.
주의할 점은, 템플릿 리터럴 타입은 ‘타입 레벨’의 코드이기 때문에, 타입레벨의 표현식이 와야한다는 점이다.
다음과 같이 문자열 값을 type에 사용할 수 없다!
const World = 'world';
type Greeting = `hello ${World}`;
//'World' refers to a value, but is being used as a type here. Did you mean 'typeof World'?(2749)
Exported type alias 'Greeting' has or is using private name 'World'.(4081)TS Playground - An online editor for exploring TypeScript and JavaScript (opens in a new tab)
템플릿 리터럴 타입과 타입 변수
템플릿 리터럴 타입은 타입 변수와 함께 사용할 수 있어 동적으로 타입을 생성하는 데 유용하다.
type MyObject = {
kind: 'string' | 'number';
value: string | number;
};
type PropType<Type, Prop extends keyof Type> = `${Prop} is ${Type[Prop]}`;
type StrType = PropType<MyObject, 'value'>; // "value is string | number"type Food = "Bread" | "Milk" | "Apple";
type WithTastyPrefix<T extends string> = `Tasty ${T}`;
// 위 타입을 사용해 특정 음식 이름에 "Tasty" 접두사를 붙임
const tastyApple: WithTastyPrefix<"Apple"> = "Tasty Apple"; // 가능
const tastyMilk: WithTastyPrefix<"Milk"> = "Tasty Milk"; // 가능
// const tastyOrange: WithTastyPrefix<"Orange"> = "Tasty Orange"; // 오류! "Orange"는 Food 타입에 정의되지 않았음Intrinsic String Manipulation Type(문자열 조작 유틸리티 타입)
TypeScript 4.1은 템플릿 리터럴 타입과 함께 사용할 수 있는 문자열 조작 유틸리티 타입을 제공한다. 예를 들어:
Uppercase<T>: 문자열 **T**의 모든 글자를 대문자로 만든다.Lowercase<T>: 문자열 **T**의 모든 글자를 소문자로 만든다.Capitalize<T>: 문자열 **T**의 첫 글자를 대문자로 만든다.Uncapitalize<T>: 문자열 **T**의 첫 글자를 소문자로 만든다.
결론
템플릿 리터럴 타입은 문자열과 타입 변수를 결합해 복잡한 타입을 표현하거나 동적으로 생성할 수 있는 강력한 기능이다. 문자열 조작과 타입 매핑과 같은 고급 타입 작업을 수행하는 데 유용하게 사용할 수 있다.
주의할 점
- 템플릿 리터럴 타입은 문자열 내에서 표현식을 사용할 수 있기 때문에, 문자열 내에 존재하는 특수문자(
\\r,\\n등)나 유니코드 문자들이 예상과 다르게 동작할 수 있다. - 이스케이프 문자를 사용하면 일부 문자를 활용할 수 있지만, 발생할 수 있는 모든 예외 상황을 고려하기는 어렵다. 따라서 템플릿 리터럴 타입을 사용할 때는 문자열 내에 특수문자나 유니코드 문자를 사용하지 않는 것이 좋다.
- 호환성: 템플릿 리터럴 타입은 상대적으로 최근에 추가된 기능이므로, 오래된 TypeScript 버전과 호환되지 않을 수 있다. 특정 프로젝트의 TypeScript 버전을 고려해야 할 수 있다.
- 복잡도: 템플릿 리터럴 타입을 너무 복잡하게 중첩하게 되면, 코드의 가독성이 떨어질 수 있으며, 오류 메시지를 해석하기 어려울 수 있다.
Conditional Type
https://www.typescriptlang.org/docs/handbook/2/conditional-types.html (opens in a new tab)
타입스크립트가 지원하는 기능 중에서도 가장 독특하다고 할 수 있는 타입.
조건부 타입은 “U, V에 의존하는 T 타입을 선언하고, T가 V의 서브타입이면 T를 A에 할당하고, 그렇지 않으면 T를 B에 할당한다”는 뜻. (말로 풀어서 쓰니 더 이해가 안되겠지만, 코드를 보면 한눈에 이해가 간다)
type IsString<T> = T extends string ? true : false- 여기서
extends키워드는string타입을 상속받는다는 뜻이 아닌,T가string의 서브타입인지에 대한 조건이다. T가string의 서브타입이면true타입, 그렇지 않으면false타입
삼항 연산자와 문법이 같다고 할 수 있고, 중첩해서 삼항연산자를 사용할 수 있다.
type TypeName<T> =
T extends string ? 'string' :
T extends number ? 'number' :
T extends boolean ? 'boolean' :
T extends undefined ? 'undefined' :
T extends Function ? 'function' :
'object';하지만 값 수준이 아닌, 타입 수준의 연산이다.
조건부 타입은 타입 별칭(type alias)외에도 타입을 사용할 수 있는 거의 모든곳에서 사용할 수 있다.
- 인터페이스 , 클래스, 매개변수 타입, 제네릭 기본값 등
분배적 조건부
유니온 타입이 사용되는 경우 수학시간에 배운 분배법칙!이 적용된다.
조건부 타입은 유니온 타입에 대해 분배된다. 예를 들어, **(A | B) extends U ? X : Y**는 **(A extends U ? X : Y) | (B extends U ? X : Y)**와 동일하다.

(string | number) extends T ? A : B
// 다음과 같음
(string extends T ? A : B) | (number extends T ? A : B)이게 왜 쓸모있는지 다음을 통해 알아보자.
type ToArray<T> = T[]
type A = ToArray<number | string> // (number | string)[]T에 유니온 타입을 전달했더니 (number | string)[] 타입이 되었다.
조건부 타입에 유니온 타입을 사용한다면?
type ToArrayConditional<T> = T extends unknown ? T[] : T[] // 그냥 결과를 보기위해서..
type A = ToArrayConditional<number | string>; // number[] | string[](number | string)[]가 아닌, number[] | string[] 와 같이 분배된 결과가 나타난다.
이처럼 조건부 타입을 사용하면 타입스크립트는 유니온 타입을 조건부의 절들로 분배한다.
⇒ 어디에 쓸 수 있는데?
⇒ 다양한 공통 연산을 안전하게 표현할 수 있다.
example
T에는 존재하지만 U에는 존재하지 않는 타입을 구하는 Without<T, U>
type Without<T, U> = T extends U ? never : T;을 다음과 같이 사용할 수 있다.
type A = Without<boolean | number | string, boolean>; // number | string이 타입이 구해지는 과정을 보면
-
조건을 유니온으로 분배함
type A = Without<boolean, boolean> | Without<number, boolean> | Without<string, boolean> -
Without의 정의를 교체하고 T와 U를 적용
type A = (boolean extends boolean ? never : boolean) | (number extends boolean ? never : number) | (string extends boolean ? never : string) -
조건을 평가
type A = never | number | string -
단순화
type A = number | string
눈치 챘을 수도 있는데, Exclude<T, U> 유틸리티가 이 덕분에 구현될 수 있었다.
Infer keyword
조건부 타입의 마지막 특성. 조건의 일부를 제네릭 타입으로 선언할 수 있는 기능이다. 여태까지 알고 있던 제네릭 타입 매개변수를 꺽쇠괄호(<T>)를 사용하는 방식과 다르게, 조건부 타입에서는 인라인으로 선언하는 문법을 제공한다. : infer 키워드
조건부 타입은 infer 키워드를 사용해 타입 추론을 수행할 수 있다.
예시
배열의 요소 타입을 얻는 ElementType 을 조건부 타입을 사용해 정의해보자.
type ElementType<T> = T extends unkonwn[] ? T[number] : T
type A = ElementType<number[]> // numberinfer keyword를 사용하면 이렇게 작성할 수 있다.
type InferedElementType<T> = T extends (infer U)[] ? U : T
type B = InferedElementType<number[]> //number;타입스크립트는 문맥을 살펴서 InferedElementType의 T에 어떤 타입을 전달했는지를 보고 U의 타입을 추론한다.
-
왜 U를 Infer키워드로 인라인으로 선언해야할까? 미리 제네릭으로 선언하면 안되나?
제네릭 타입 매개변수로 선언한다면, 다음과 같이 사용할 때 직접 명시해줘야한다.
type ElementUgly<T, U> = T extends U[] ? U : T; typet C = ElementUgly<number[], number>근데 이렇게하면 ElementUgly를 정의하는 의미가 없어진다. 수동으로 계산하는 것과 마찬가지.
이미 indexed Access Type에서 배열의 요소 타입을 [number] 를 통해 접근할 수 있다는 것을 배워서, 이 예시는 와닿지 않을 수 있다.
조금 복잡한 예시를 보자면
type GetSecondarArg<F> = F extends (a: any, b: infer B) => any ? B : never
// 두번째 매개변수를 가져옴
type F = GetSecondarArg<typeof Array['prototype']['slice']>
// array.slice() 메소드의 두번째 매개변수를 가져왔다.이처럼 특정 함수의 매개변수가 어떤 타입인지 컴파일 타임에 알 수 있다.
내장 조건부 타입들
조건부 타입을 사용하면 강력한 연산자 몇 가지를 타입 수준에서 표현할 수 있다!
타입스크립트는 전역에서 바로 사용할 수 잇는 여러 조건부 타입을 다음과 같이 제공한다.
Exclude<T, U>
이전에 살펴본 Without<T, U> 와 같이 T에 속하지만 U에 없는 타입
-
Extract<T, U>T타입 중 U에 할당할 수 있는 타입을 구한다.
-
NonNullable<T>T에서
null,undefined타입을 제외한다. -
ReturnType<F>함수의 반환 타입을 구한다.(제네릭, 오버로드된 함수에서는 동작하지 않음)
-
InstanceType<C>클래스 생성자의 인스턴스 타입을구한다.
더 많은 유틸리티 타입이 궁금 | 기억안난다면 Utility Type (opens in a new tab) 여기서 확인하세요!
결론
- 조건부 타입은 TypeScript의 매우 강력한 기능 중 하나로, 타입의 형태를 동적으로 결정할 수 있게 해준다.
- 유니온 타입에 대한 분배 규칙과 함께 사용하면 매우 복잡한 타입 작업도 할 수 있다.
- 이를 통해 코드의 유연성과 재사용성을 높일 수 있다.
infer키워드는 조건부 타입에서 사용가능하며, 전달된 T 매개변수의 타입에 따라 추론가능.- 조건부 타입을 활용한 타입스크립트 내장 유틸리티가 꽤 있다.
상황에 따라서는타입을 완벽하게 지정하지 안혹도 안전하다는 사실을 타입스크립트에게 믿게 하고 싶을 때가 있다. 예를들어 third party 모듈의 타입 정의가 잘못되었다면.. 이를 대비해서 타입스크립트는 몇 가지 탈출구를 제공한다.
Type Assertion
타입 단언을 사용할 수 있는 조건
타입 B가 있고 B는 A의 서브타입, C의 슈퍼타입이라면 타입 검사기에게 B가 A라거나 C라고 어서션(단언)할 수 있다. 즉, 어떤 하나의 타입은 자신의 슈퍼타입이나 서브타입으로만 어서션할 수 있다.
예를 들어, number, string은 서로 슈퍼타입, 서브타입 관계가 아니므로 어서션 할 수 없다.
타입 단언 문법
우리가 자주 사용하는 그것! as
그리고 익숙지 않은 꺽쇠 괄호.. <?>
예시
const myCanvas = <HTMLCanvasElement>document.getElementById("main_canvas");
const myCanvas = document.getElementById("main_canvas") as HTMLCanvasElement;Example
const formatInput = (input: string) => {
// ...
}
cosnt getUserInput = (): string | number => {
// ..
}
const input = getUserInput() // string | number
formatInput(input);
// error: Argument of type 'string | number' is not assignable to parameter of type 'string'.
// Type 'number' is not assignable to type 'string'.이 때 우리가 getUserInput()의 리턴값으로 얻은 input 변수가 string이라는 것을 알고 있고, 이를 타입 검사기에 확실하게 단언하고싶을 때, 아래와 같이 사용할 수 있다.
formatInput(input as string);
// 위와 같음
formatInput(<string>input); 타입 주석과 마찬가지로, Type Assertion은 컴파일러에 의해 제거되며 코드의 런타임 동작에 영향을 주지 않는다.
꺽쇠 괄호문법은 .tsx 파일 내에서는 작동하지 않는다.
-
우리는 리액트 개발하면서 JSX문법을 사용하기 때문에 이 문법은 사용할 일이 잘 없다.
-
Tslint rule중에도
noangle-bracket-type-assertion이 있고, 이 규칙을 사용하면 꺽쇠괄호 문법을 사용할 수 없다.
as unkown as ~
만약 두 타입 사이 연관성이 충분하지 않아서 한 타입을 다른 타입으로 단언할 수 없을 땐 어떡하나?
위에서 말했듯 서브타입 관계가 아니면 단언이 불가능하다.
const x = "hello" as number; //error양쪽 타입이 서로에게 충분히 겹치지 않기 때문에 잘못될 수 있지만 의도적인 경우, 먼저 표현식을 'unknown 혹은 any'으로 변환하라.'고 타입스크립트 핸드북에서 말한다.
때로는 이 규칙이 지나치게 보수적일 수 있으며, 유효할 수 있는 더 복잡한 형변환을 허용하지 않을 수 있다. 이런 경우에는 먼저 any(또는 나중에 소개될 unknown)로, 그리고 원하는 타입으로 두 번의 어설션을 사용할 수 있다:
const a = (expr as any) as T;
const numberTypeString = 'dfdaf' as unkown as number;이렇게 우회하는 타입 어서션은 되도록 피하는게 좋다.
Non-null Assertion Operator (Postfix!)
타입스크립트는 어떤 값의 타입이 null | undefined가 아닌 T라고 단언하는 특수문법을 제공한다.
개발 하다가, 특정 변수가 nullable하지 않다는 것을 충분히 아는데, 타입스크립트는 몰라줘서 답답할 때가 있었을 것이다.
이 때 우리는 다음과 같이 분기처리를 해주어 타입을 걸러주곤 한다.
const closeDialog = (dialog: Dialog) => {
if (!dialog.id) {
return;
}
// something logic
};하지만 이러한 처리 없이, 단순히 ! postfix를 사용해서 해당 값이 null이나 undefined가 아니라고 알려줄 수 있다.
function liveDangerously(x?: number | null) {
// No error
console.log(x!.toFixed());
}
다른 타입 어서션들과 같이 nonnull 어서션 연산자도 런타임 코드에 영향을 미치지 않는다.
따라서 특정 값이 null, undefined가 될 가능성이 없을 때 사용하는 것이 좋다.
“단언”은 매우 강력한 기능이고, 조심해서 사용해야한다.
ESLInt rule에 @typescript-eslint/no-non-null-assertion 를 통해 nonnull assertion을 사용하지 못하게 강제할 수 있다(현재 엘리스에서도 사용 중)
확실한 할당 어서션
let userId: string;
userId.toUpperCase(); //error
// 값을 할당하지 않음.let userId!: string;
userId.toUpperCase(); //error가 발생하지 않음.타입 어서션, Nonnull 어서션과 마찬가지로 확실한 할당 어서션을 자주 사용하고 있다면 뭔가 잘못 되어가고 있다는 신호이다. 코드를 리팩토링하자.
as const
**as const**는 TypeScript에서의 특별한 구문으로, 변수나 객체를 리터럴 타입으로 추론하게 만든다.
이는 객체, 배열, 그리고 문자열과 숫자 값에 대해서도 사용할 수 있으며, 타입 추론을 더 엄격하게 만들어 준다.
사용 예시
-
상수 리터럴 타입: 변수에 **
as const**를 사용하면, 그 값이 변하지 않는 상수 리터럴 타입으로 간주된다.const value = 42 as const; // type: 42 -
읽기 전용 객체: 객체 리터럴에 **
as const**를 사용하면, 객체의 모든 프로퍼티가읽기 전용이 되고 리터럴 타입으로 추론된다.const obj = { x: 10, y: 20 } as const; // type: { readonly x: 10, readonly y: 20 } -
읽기 전용 배열: 배열 리터럴에 **
as const**를 사용하면, 배열이읽기 전용이 되고, 각 요소가 리터럴 타입으로 추론된다.const arr = [1, 2, 3] as const; // type: readonly [1, 2, 3]
Literal Inference
리터럴 객체 / 배열을 사용하는 경우, 타입스크립트는 해당 객체/ 배열의 요소, 프로퍼티가 변경될 수 있다고 가정한다
const obj = { counter: 0 };
if (someCondition) {
obj.counter = 1;
}
타입스크립트는 0이었던 counter가 1로 변경되었다고 해서 에러를 던지지 않는다.
다시말해서, Obj.counter는 number 타입을 가지지, 0 타입을 가지지 않는다는 말이다.
타입은 읽는 것 뿐아니라 쓰는것에도 사용되기 때문이다.
declare function handleRequest(url: string, method: "GET" | "POST"): void;
const req = { url: "https://example.com", method: "GET" } as const;
handleRequest(req.url, req.method);
// error : Argument of type 'string' is not assignable to parameter of type '"GET" | "POST"'. 위 예시에서 **req.method**는 **"GET"**이 아닌 **string**으로 추론된다.
**req.method**에 새 문자열 **"GUESS"**가 할당될 수 있기 때문에, TypeScript는 이 코드에 오류가 있다고 판단한다.
이 문제를 해결하기 위한 두 가지 방법이 있다.
-
두 위치 중 하나에 타입 어설션을 추가하여 추론을 변경할 수 있다:
// 변경 1: const req = { url: "https://example.com", method: "GET" as "GET" }; // 변경 2 handleRequest(req.url, req.method as "GET");변경 1은 "**
req.method**는 항상 리터럴 타입 **"GET"**을 가져야 한다"는 의미로, 이후 해당 필드에 **"GUESS"**가 할당될 가능성을 방지한다. 변경 2는 "다른 이유로 **req.method**의 값이 **"GET"**임을 알고 있다"는 의미다. -
전체 객체를 타입 리터럴로 변환하기 위해 **
as const**를 사용할 수 있다:csharpCopy code const req = { url: "https://example.com", method: "GET" } as const; handleRequest(req.url, req.method);
as const 접미사는 타입 시스템에 대해 **const**와 같이 작동한다.
특징
- **
as const**를 사용하면 타입 추론이 더욱 엄격해지며, 읽기 전용으로 만들 수 있음 - 변수나 프로퍼티를 변경하려 할 때 컴파일 타임 에러를 발생시킴
- 상수 리터럴 값을 다루거나 객체와 배열을 불변으로 다루는 데 유용함.
is 연산자(사용자 정의 타입)
is 연산자는 TypeScript에서 사용자 정의 타입 가드를 정의하는 데 사용되는 특별한 연산자다.
a is string 형태로 사용.
사용자 정의 타입 가드는 특정 변수가 특정 타입임을 확인하는 런타임 검사를 수행하는 함수다.
is 연산자를 사용하면 함수가 특정 타입을 가진 변수임을 확인하고 그 결과를 타입 시스템에 알릴 수 있다.
예시
function isNumber(value: any): boolean {
return typeof value === "number";
}
const value: any = 42;
if (isNumber(value)) {
console.log(value + 1); // 43
}위의 코드에서 isNumber 함수는 **value**가 number 타입인지 확인한다.
이 함수 내부에서 **value is number**는 **value**가 number 타입임을 선언하는 타입 가드다.
사용 시 주의 사항
- 타입 가드 함수 내에서 실제 런타임 검사를 수행해야 한다. 즉, 단순히 타입을 선언하기만 하면 안 되고, 실제 해당 타입이 맞는지 확인하는 로직이 필요하다.
is연산자는 함수의 반환 타입에만 사용할 수 있다.
위의 예제를 보면 굳이 왜쓰나 싶겠지만, 아래의 예시를 보면..
type Species = 'cat' | 'dog';
interface Pet {
species: Species;
}
class Cat implements Pet {
public species: Species = 'cat';
public meow(): void {
console.log('Meow');
}
}
function petIsCat(pet: Pet): **pet is Cat** {
return pet.species === 'cat';
}
// boolean 타입을 리턴한다고 명시하는 경우
function petIsCatBoolean(pet: Pet): boolean {
return pet.species === 'cat';
}
const p: Pet = new Cat();
p.meow(); // ERROR: Property 'meow' does not exist on type 'Pet'.
// Pet타입이지만 Cat타입인지는 알 수 없음
if (petIsCat(p)) {
p.meow(); // now compiler knows for sure that the variable is of type Cat and it has meow method
// petIsCat()을 통해 매개변수가 Cat 타입임을 컴파일러에게 알려줌.
}
if (petIsCatBoolean(p)) {
p.meow(); // ERROR: Property 'meow' does not exist on type 'Pet'.
// petIsCatBoolean()의 리턴 타입이 boolean이라는 것만 알 뿐, p의 타입에 대한 정보는 없음.
}따라서, 타입 가드도 하면서 해당 변수가 특정한 타입을 만족하는지까지 컴파일러에게 알려주고 싶을 때 사용할 수 있다.
타입스크립트 스터디를 마치며 (feat. ChatGPT)
길고도 짧았던 5월부터 8월 중순까지의 타입스크립트 스터디가 끝났습니다. 스터디 시작 쯤엔 ‘타입스크립트 심화’라는 주제로 깊이있게 주제를 다루고 싶었는데, 뒤로 갈 수록 그러지 못한 것 같아 아쉬우면서도 부족했던 개념들을 같이 알아갈 수 있어서 좋았습니다 이 스터디가 여러분에게도 의미 있었기를 바랍니다.
🚀 스터디의 성과
- 기본 개념 이해: TSConfig의 이해, 함수 오버로드, 제네릭, 유틸리티 타입, 고급 타입 등 타입스크립트에서 제공하는 다양하고 멋진 기능들을 알아보았습니다.
- 고급 타입 활용: 유니온 타입, 제네릭, 맵드 타입 등 고급 타입을 활용하여 더 풍부하고 유연한 코드를 작성하는 법을 배웠습니다.
- 활용 예시: 개념 뿐 아니라 실제 프로젝트에서 어떻게 적용될 수 있는지 확인해봤습니다.