[TS] 다시 정리하는 TypeScript - 2

Ganziman·2025년 7월 29일
post-thumbnail

안녕하세요! 😊
타입스크립트를 공부하면서 헷갈렸던 부분들을 직접 정리해보았습니다.
개인적인 학습 기록이지만, 누군가에게 도움이 되셨으면 좋겠습니다! 🙇🏻‍♂️

이번 포스팅은 [TS] 다시 정리하는 TypeScript - 1에 이어 작성한 글입니다.

왜 다시 공부하려고 했을까?

현재 실무에서 TypeScript를 사용한 지 어느덧 2년을 바라보고 있습니다. 다양한 프로젝트를 경험하면서 동료 개발자들과의 협업도 많아졌고, 자연스럽게 '타입을 더 명확하고 유연하게 설계하는 방법은 없을까?'라는 고민이 생기기 시작했습니다.

간단하게 예를 들어, 아래와 같은 구조를 만들었던 적이 있습니다.

type Person = {
  name: string;
  age: number;
  gender: "MALE" | "FEMALE";
  address: string;
  job: "developer" | "student" | "designer";
};

type PersonSummary = {
  name: string;
  age: number;
  address: string;
};

당시에는 구조가 조금만 달라도 별도로 타입을 만들곤 했는데, 나중에 Omit 유틸리티 타입을 활용하면 더 간결하게 표현할 수 있다는 것을 알게 되었습니다.

type PersonSummary = Omit<Person, "gender" | "job">;

이처럼 타입스크립트에는 실무에서 더 유연하게 활용할 수 있는 기능들이 많다는 걸 새삼 느끼게 되었고, 이를 체계적으로 공부해보고 싶다는 생각이 들었습니다.

마침 한 입 챌린지 6기가 열리게 되어, 타입스크립트를 주제로 참여하게 되었습니다!
혼자 공부하면 느려질 때도 있고 지루해질 수 있는데, 이번 기회를 통해 집중력 있게 학습하고자 합니다.

이번 포스팅은 Day 8부터 Day 14까지, 챌린지 수업과 퀴즈를 따라가며 배운 내용과 인사이트를 정리해보려고 합니다!

Day 8️⃣

8일차는 함수 오버로딩, 사용자 정의 타입 가드, 인터페이스확장 방식을 중심으로 TypeScript의 고급 타입 시스템을 정리했습니다.

🔁 함수 오버로딩 (Function Overloading)

하나의 함수를 매개변수의 개수나 타입에 따라 여러 버전으로 정의하는 방식
하나의 함수 func 모든 매개변수의 타입 number

  • ver1. 매개변수가 1개 -> 이 매개변수에 20을 곱한 값 출력
  • ver2. 매개변수가 3개 -> 이 매개변수들을 다 더한 값을 출력
// 오버로드 시그니처
function func(a: number): void;
function func(a: number, b: number, c: number): void;

// 구현 시그니처(실제 구현부)
function func(...args: number[]): void {
  if (args.length === 1) {
    console.log(args[0] * 20);
  } else if (args.length === 3) {
    console.log(args[0] + args[1] + args[2]);
  }
}

func(1);         // ✅ 20 출력
func(1, 2, 3);   // ✅ 6 출력
func();          // ❌ 매개변수 없음
func(1, 2);      // ❌ 시그니처 없음

⚠ 오버로드를 사용할 땐 정의한 오버로드 시그니처 중 하나와 반드시 일치해야 합니다. 실제 구현 시그니처는 가장 마지막에 하나만 존재합니다.
오버로드 시그니처 func를 정의해줌으로써 앞으로 func는 이렇게 정의하겠다 라는 식으로 정의한 것

만일, 다음과 같이 구현부를 정의하게 된다면
function func(a: number, b:number, c:number){} 

아래의 오버로드 시그니처의 의미가 없어집니다.
function func(a: number): void;

🔐 사용자 정의 타입 가드 (Custom Type Guard)

type Dog = {
  name: string;
  isBark: boolean;
};

type Cat = {
  name: string;
  isScratch: boolean;
};

type Animal = Dog | Cat;

// 사용자 정의 타입 가드
function isDog(animal: Animal): animal is Dog {
  return (animal as Dog).isBark !== undefined;
}

// 반환값이 true이면 매개변수로 받은 또는 전달한 animal이 Dog 타입이구나 라고 명시한다.
function warning(animal: Animal) {
  if ("isBark" in animal) {
    // 🐶 강아지
  } else if ("isScratch" in animal) {
    // 🐱 고양이
  }
}

animal is Dog는 타입스크립트에게 조건이 true일 경우 animal이 Dog 타입임을 단언하는 역할을 합니다.

🧾 인터페이스 (Interface)

타입에 이름을 지어주는 또 다른 문법

  • 객체의 구조를 정의하는데 특화된 문법
interface Person {
  name: string;
  age: number;
}

const person: Person = {
  name: "김범수",
  age: 27,
};


❓ 인터페이스에서 함수 오버로딩은 어떻게?

함수 타입 표현식 ❌ - 이렇게 쓰면 오버로드 인식 안됨
interface Person {
  sayHi: () => void;
  sayHi: (a: number, b: number) => void;
}

✔ 호출 시그니처 방식 ✅
interface Person {
  sayHi(): void;
  sayHi(a: number, b: number): void;
}


interface Person {
    name: string;
    age: number;
    sayHi(): void;
    sayHi(a: number, b: number): void;
}

person.sayHi();
person.sayHi(1,2);


메서드의 오버로딩을 사용하고 싶다면 다음과 같이 호출 시그니처를 써야함
sayHi(): void;
sayHi(a: number, b: number): void;
항목typeinterface
유니온()/인터섹션(&) 가능
선언 병합
확장 가능성제한적유연함
객체 구조 정의가능특화됨

🔗 인터페이스 확장 (Extends)

interface Animal {
  name: string;
  age: number;
}

interface Dog extends Animal {
  isBark: boolean;
}

interface Cat extends Animal {
  isScratch: boolean;
}

interface Chicken extends Animal {
  isFly: boolean;
}

🌐 다중 확장

interface DogCat extends Dog, Cat {}

const hybrid: DogCat = {
  name: "하이브리드",
  age: 3,
  isBark: true,
  isScratch: false,
};

📚 인터페이스 선언 병합 (Declaration Merging)

동일한 이름의 인터페이스를 여러 번 선언하면 자동으로 병합

interface Person {
  name: string;
}

interface Person {
  age: number;
}

const person: Person = {
  name: "김범수",
  age: 27,
};

🔧 모듈 보강 (Module Augmentation)

이미 존재하는 인터페이스를 추가적으로 확장할 때 사용

interface Lib {
  a: number;
  b: number;
}

interface Lib {
  c: string;
}

const lib: Lib = {
  a: 1,
  b: 2,
  c: "추가됨",
};

🔤 인터페이스 작명 관습

interface IPerson {
  name: string;
}

type Type1 = number | string;
타입과 다르게 interface는 유니온이나 intersection을 만들 수 없다.
그래서, 사용할 거면 타입에다가 별칭을 사용하거나 그런식으로 사용을 해야한다.

interface의 이름을 정의할 때 앞에다가 'I'Person 과 같은 I를 쓰는 관습들이 있다. 하지만 논란이 있다.
헝가리안 표기법이라고 하는데 js 프로그래밍기법을 잘 안쓴다.
'_' 이거나 파스칼, 카멜 스네이크를 쓰는데 헝가리안까지 써야하나? 라는 이야기가 있다.

Day 9️⃣

Day 9는 클래스와 상속, 접근 제어자, 인터페이스 구현 등 객체지향 개념을 TypeScript 문법과 함께 정리한 날입니다.

🏗️ 자바스크립트의 클래스

클래스란 객체를 만들어내는 틀
붕어빵이 객체라면, 붕어빵 기계가 클래스

let studentA = {
  name: "김범수",
  grade: "A+",
  age: 27,
  study() {
    console.log("열심히 공부");
  },
  introduce() {
    console.log("헬로");
  },
};

class Student {
  name;
  grade;
  age;

  constructor(name, grade, age) {
    this.name = name;
    this.grade = grade;
    this.age = age;
  }

  study() {
    console.log(`${this.name} 공부 중...`);
  }

  introduce() {
    console.log("안녕~");
  }
}

const studentB = new Student("김범준", "B", 27);
studentB.study();      // 김범준 공부 중...
studentB.introduce();  // 안녕~


class StudentDeveloper extends Student {
    faboriteSkill;

    // 생성자 - 실제로 객체를 생성을 하는 메서드이다.
    constructor(name, garde, age, favoriteSkill){
        // 부모클래스의 생성자를 호출합니다.
        super(name, grade, age)
        this.favoriteSkill = favoriteSkill
    }

    const studentDeveloper = new StudentDeveloper("김범수", "A", 28, "Javascript")
}

타입스크립트 클래스

타입스크립트 클래스는 자바스크립트의 클래스 + 타입 시스템을 결합한 형태입니다.

const employee = {
    name:"김범수",
    age:27,
    position:"developer",
    work(){
        console.log("일함")
    }
}

class Employee {
  name: string;
  age: number;
  position: string;

  constructor(name: string, age: number, position: string) {
    this.name = name;
    this.age = age;
    this.position = position;
  }

  work() {
    console.log("열심히 일함");
  }
}


class ExecutiveOfficer extends Employee {
    // 필드
    officeNumber: number;


    // 생성자
    // 파생 클래스의 생성자는 super 호출을 포함해야 한다.
    constructor(name:string, age:number, position:string; officeNumber:number){
        super(name,age,position);
        this.offceNumber = officeNumber;
    }
}


const employeeB = new Employee("김범수",27,"개발자");


// 타입스크립트의 클래스는 타입으로도 활용할 수 있다.
const employeeC :Employee = {
    name:"",
    age:0,
    position:"",
    work(){

    }
}

접근 제어자 - aceess modifier

특정 필드나 메서드의 접근할 수 있는 범위를 설정하는 문법
=> public private protected

private를 사용하면, 외부에서, 파생클래스에서도 this.name으로 접근 안된다.
만약 파생클래스에서라도 접근 허용하려면 protected(Public private 중간인 느낌) 접근제어자를 사용하자.

class User {
  public name: string;
  private password: string;
  protected email: string;

  constructor(name: string, password: string, email: string) {
    this.name = name;
    this.password = password;
    this.email = email;
  }
}

class PremiumUser extends User {
  showEmail() {
    console.log(this.email); // ✅ 가능
    // console.log(this.password); // ❌ private이므로 접근 불가
  }
}
키워드설명
public어디서든 접근 가능 (기본값)
private클래스 내부에서만 접근 가능
protected클래스 내부 + 자식 클래스에서만 접근 가능

인터페이스와 클래스

클래스가 인터페이스를 구현(implements) 하면, 해당 인터페이스가 요구하는 구조를 따라야 합니다.

interface CharacterInterface {
    name:string;
    moveSpeed:number;
    move(): void;
}

// CharacterInterface는 설계도라고 하여 캐릭터 class는 해당 설계도를 구현한다는 의미
class Character implements CharacterInterface{
    name:string;
    moveSpeed:number

    constructor(name:string, moveSpeed:number){
        this.name=name
        this.moveSpeed = moveSpeed
    }

    move(): void{
        consolel.log(`${this.moveSpeed} 속도로 이동`)
    }

    // interface로 정의하는 필드들은 무조건 public만 정의할 수 있기때문.
}

Day 🔟

제네릭(Generic)의 기본 개념부터 실전 응용까지 정리한 날입니다.

제네릭

일반적인 포괄적인 함수 - 모든 타입에 두루두루 사용할 수 있는 범용적인 함수나 타입을 만드는 방법

function func<T>(value: T): T {
    return value;
}
  • <T>는 타입 변수(Type Variable) 로, 실제 타입은 함수를 호출할 때 결정됨

  • 타입 변수와 함께 타입의 값을 인수로 받아서 범용적으로

let a = func<number>(10);    // T = number
let b = func<string>("hi");  // T = string

⚙️ 제네릭 응용: 타입 변수 여러 개

function swap(a: any, b: any){
    return [b, a];
}

const [a, b] = swap(1, 2);
=> swap("1", 2); 하면 타입 오류 발생.

a와 b의 타입이 같을 수 있지만 다를 수 있는 상황에서는 다른 타입은 타입 변수를 하나만 쓰는게 아닌 두개를 쓰자.

<T, U> => 바꾸면서 (a: T, B:U)


function swap<T, U>(a: T, b: U): [U, T] {
  return [b, a];
}

const [x, y] = swap("hello", 123); // [123, "hello"]
function returnFirstValue<T>(data: T[]): T {
  return data[0];
}

const val = returnFirstValue([1, 2, 3]); // val: number

// 첫 번째 요소만 특정 타입으로 제한하고 싶을 때
function returnFirstValue<T>(data: [T, ...unknown[]]): T {
  return data[0];
}

const val = returnFirstValue([1, "hello", true]); // val: number

// 첫 번째 요소만 특정 타입으로 제한하고 싶을 때

function returnFirstValue<T>(data: [T, ...unknown[]]): T {
  return data[0];
}

const val = returnFirstValue([1, "hello", true]); // val: number

[T, ...unknown[]]: 첫 번째 요소는 T, 나머지는 어떤 타입이든 가능

첫 번째 요소만 타입을 고정하고 나머지는 무시 가능

let str = returnFirstValue([1,"hello","mynameis"]);
=> str의 타입 추론 결과는 string|number가 되며 T는 [number,string][] 튜플타입이 된다.
  • 만약 첫번째 요소가 number로 타입을 추론하고 싶으면 <T>(data: [T, ...unknown[]])을 하게 되고 T첫번째 요소가 number이고 나머지는 몰라도 되는 것
  • 첫 번째 요소만 타입을 고정하고 나머지는 무시 가능
// length 속성을 가진 값만 허용
function getLength<T extends { length: number }>(data: T): number {
  return data.length;
}

getLength([1, 2, 3]);            // ✅ Array
getLength("hello");             // ✅ String
getLength({ length: 10 });      // ✅ 객체
getLength(123);                 // ❌ number는 length 속성이 없음



<T extends { length: number}>(data:T)
: T를 length 타입이 number 타입으로 가지고 있는 객체를 확장하는 타입으로 제한한다.
  • extends 를 사용하여 타입의 범위를 제한(bounded generic) 할 수 있음

🧩 제네릭으로 고차 함수 구현하기

map 메서드 타입 정의하기

function map<T, U>(arr: T[], callback: (item: T) => U): U[] {
  let result: U[] = [];
  for (let i = 0; i < arr.length; i++) {
    result.push(callback(arr[i]));
  }
  return result;
}

// 사용 예시
const doubled = map([1, 2, 3], (item) => item * 2); // number[]
const uppercased = map(["hi", "hello"], (item) => item.toUpperCase()); // string[]
  • T: 입력값 타입
  • U: 반환값 타입

forEach 함수 타입 정의

function forEach<T>(arr: T[], callback: (item: T) => void): void {
  for (let i = 0; i < arr.length; i++) {
    callback(arr[i]);
  }
}

// 사용 예시
forEach([1, 2, 3], (item) => {
  console.log(item.toFixed()); // item은 number로 추론됨
});
항목설명
T, U타입 변수 (Generic Type Parameters)
<T>타입스크립트에서 제네릭 사용의 기본 문법
T extends ...타입 변수의 범위 제한
[T, ...unknown[]]첫 요소만 고정하고 나머지 무시
map, forEach고차 함수 구현에도 제네릭 활용 가능

Day 1️⃣1️⃣

제네릭 인터페이스, 제네릭 타입 별칭, 인덱스 시그니처, 제네릭 클래스, Promise 반환 타입 등 제네릭의 다양한 실전 활용 방법을 정리한 날입니다.

제네릭 인터페이스

interface KeyPair<K, V> {
    key: K;
    value: V;
}

let keyPair : KeyPair<string,number> {
    key: "key",
    value:0,
}

// 제네릭 인터페이스를 사용할 때, 타입 변수에 할당할 타입을 꺽새와 함께 반드시 사용해야합니다.

타입변수
= 타입 파라미터
= 제네릭 타입 변수
= 제네릭 타입 파라미터

let keyPair: KeyPair<boolean, string[]> = {
    key:ture,
    value: ["1"],
}

인덱스 시그니처

interface NumberMap {
    [key:string]: number;
}

interface Map<V>{
    [key:string]: V
}

let stringMap: Map<string> = {
    key:"value",
}

let booleanMap: Map<boolean> = {
    key: true,
}

제네릭 타입 별칭

type Map2<V>{
    [key:string]: V
}


let stringMap2 : Map2<string> = {
    key:"",
}

제네릭 인터페이스의 활용 예시

interface Student {
    type: "student";
    school:string;
}

interface Developer {
    type: "developer",
    skill: string;
}

interface User<T>{
    name:string;
    profile: T;
}

const developerUser: User<Developer> ={
    name:"김범수",
    profile: {
        type:"develper",
        skill:"TypeScriptio"
    }
}

const studentUser: User<Student>={
    name:"김범수",
    profile: {
        type:"sutdnet",
        school:"한림대"
    }
}

function goToSchool(user: User<Student>){
    if(user.profile.type !== "student"){
        console.log("잘 못 오셨습니다.")
        return;
    }
    const school = user.profile.school;
    console.log(`${school}로 등교 완료`)
}

타입 좁히기 코드를 쓸 필요가 없음

제네릭 클래스

class List<T> {
    consturctor(private list: T[]){}

    push(data: T){
        this.list.pushdata()
    }
    pop(){
        this.list.pop()
    }

    print(){
        console.log(this.ist)
    }
}


const numberList = new List([1,2,3]);

제네릭 클래스는 제네릭 인터페이스, 타입과 다르게 생성장에 인수로 전달하는 값을 기준으로 타입의 값을 추론한다.

프로미스

const promise = new Promise<number>((resolve,reject) => {
    setTimeout(() => {
        resolve(20);
    },3000)

    promise.then((response) => {
        console.log(response ); // 20
    })
})

### 프로미스를 반환하는 함수의 타입을 정의

interface Post {
    id: number;
    title: string;
    content: string;
}

function fetchPost(): Promise<Post>{
    return new Promise((resolve,reject) =>{
        setTimeout(() => {
            resolve({
                id: 1,
                title: "게시글 제목",
                content: "게시글 컨텐츠",
            })
        },3000)
    })
}

const postRequest = fetchPost();

postRequest.then((post) => {
    post.id
})

Day 1️⃣2️⃣

Indexed Access Type, keyof, Mapped Type, 템플릿 리터럴 타입 등 TypeScript의 고급 타입 조작 문법들을 정리한 날입니다.

📌 인덱스드 엑세스 타입 (Indexed Access Type)

객체, 배열, 튜플 타입에서 특정 속성의 타입을 추출하는 문법

interface Post {
  title: string;
  content: string;
  author: {
    id: number;
    name: string;
    age: number;
  };
}

// 특정 속성의 타입을 추출
function printAuthorInfo(author: Post["author"]) {
  console.log(author.name);
}
  • Post["author"]는 author의 타입을 추출함
  • Post["author"]["id"]를 쓰면 id의 타입(number) 추출 가능

// 'author'는 변수, 값이 들어갈 수 없고 타입만 명시할 수 있다. ex) const key =  "author" X 안됨

const post: Post = {
    title:"게시글 제목",
    content: "게시글 본문",
    author: {
        id:1,
        name: "김범수"
        age:27
    }
}

id값 뽑아 오는 방법
Post["author"]["id"]


type PostList {
    title:string;
    content:string;
    author: {
        id: number;
        name: string;
        ag: number
    }
}[];

const post:PostList[number] or PostList[0] = {

}
// 배열 타입으로부터 하나의 요소만 가져온다는 것.


type Tup = [number, string, boolean];

type First = Tup[0]; // number
type Second = Tup[1]; // string
type Union = Tup[number]; // number | string | boolean

type Tup3 = Tup[3]  // X

🔑 keyof 연산자

객체 타입의 key들을 유니온 타입으로 반환하는 연산자

interface Person {
  name: string;
  age: number;
}

type PersonKeys = keyof Person; // "name" | "age"

function getProperty(person: Person, key: keyof Person) {
  return person[key];
}

const person: Person = {
    name:"김범수",
    age: 27
}
  • 참고: keyof typeof person 로도 사용 가능

🔁 맵드 타입 (Mapped Type)

기존 타입을 바탕으로 속성 하나하나를 반복하며 변형

interface User {
  id: number;
  name: string;
  age: number;
}

type BooleanUser = {
  [key in keyof User]: boolean;
};

// 예제: 모든 속성을 readonly로 만들기
type ReadonlyUser = {
  readonly [key in keyof User]: User[key];
};

// 예제: 선택적 속성으로 만들기 (Partial)
type PartialUser = {
  [key in keyof User]?: User[key];
};
- 내장 제네릭 Partial<T> 와 동일합니다.

function updateUser(user: Partial<User>) {
  // id, name, age 중 일부만 받아도 OK
}

🧩 템플릿 리터럴 타입

문자열을 조합해서 새로운 문자열 타입을 생성할 수 있는 기능

type Color = "red" | "black" | "green";
type Animal = "dog" | "cat" | "chicken";

type ColoredAnimal = `${Color}-${Animal}`;
// "red-dog" | "red-cat" | "red-chicken" | "black-dog" | ...

문자열로 여러 상황들을 표현할 떄 사용된다.
  • 실무에서는 이벤트 명("click-user"), 상태 키("is-loading"), API 키 조합 등에 많이 사용됩니다.
  • interface에서는 템플릿 리터럴 타입을 사용할 수 없다
문법설명
T[K]특정 속성의 타입 추출
keyof T객체 타입의 key만 추출
[K in keyof T]속성 반복 → 변형 (Mapped Type)
`${A}-${B}`문자열 조합으로 타입 생성

Day 1️⃣3️⃣

조건부 타입(T extends U ? A : B), 분산 조건부 타입, infer를 활용한 타입 추론 등 타입스크립트의 고급 타입 제어 문법을 집중적으로 학습한 날입니다.

🎯 조건부 타입 (Conditional Type)

type A = number extends string ? string : number; // A: number

type ObjA = { a: number };
type ObjB = { a: number; b: number };

type B = ObjB extends ObjA ? number : string; // B: number
  • A extends B ? C : D 구조로 작성됨

  • extends는 서브타입 관계를 검사

  • 조건이 true면 C, false면 D를 결과 타입으로 반환

💡 제네릭과 조건부 타입

type StringNumberSwitch<T> = T extends number ? string : number;

let varA: StringNumberSwitch<number>; // string
let varB: StringNumberSwitch<string>; // number
  • 호출 시 전달된 제네릭 타입에 따라 결과가 바뀜
  • 매우 범용적인 타입 설계가 가능

예시: 문자열만 처리하는 함수 타입 정의

function removeSpaces<T>(text: T): T extends string ? string : undefined;

function removeSpacesImpl(text: any) {
  if (typeof text === "string") {
    return text.replaceAll(" ", "") as any;
  }
  return undefined as any;
}

const result1 = removeSpacesImpl("hi im winterlood"); // string
const result2 = removeSpacesImpl(undefined); // undefined

분산적인 조건부

type StringNumberSwitch<T> = T exjtends number ? string : number;

let varA : StringNumberSwitch<number> // string
let varB : StringNumberSwitch<string> // number

let c : StringNumberSwitch<number|string>;
// 이렇게 사용하면 한번은 StringNumberSwitch<number> 이렇게, 나머지 한번은 StringNumberSwitch<string> 이렇게 들어간다 그 결과를 유니온으로 묶는다.

그래서 결과가 타입은 string | number로 된다
  • T가 유니온 타입이면 조건부 타입이 각 요소에 개별적으로 적용됨

  • 이걸 분산(distributed) 된다고 표현함

let d: StringNumberSwitch<boolean | number | string>;

// 1단계
StringNumberSwitch<boolean>  |
StringNumberSwitch<number> |
StringNumberSwitch<string>

// 2단계
number |
string |
number

// 결과: number | string

분산을 막는 방법

ts
복사
편집

type NonDistributive<T> = [T] extends [number] ? string : number;

type A = NonDistributive<number | string>; // number

실용적인 예제 : union에서 특정 타입을 제거하는 기능

🧹 Exclude<T, U>

  • T에서 U를 제거
type Exclude<T, U> = T extends U ? never : T;

type A = Exclude<number | string | boolean, string>;

// 단계적으로 보면
// Exclude<number, string> → number
// Exclude<string, string> → never
// Exclude<boolean, string> → boolean


// 결과: number | boolean
  • never는 왜 없나? never라는 아무것도 없는 공집합을 합집합 하면 number | boolean 이 나온다.

Extract<T,U>

  • T에서 U에 해당하는 타입만 추출
type Extract <T, U> = T extends U ? T : never ;
type B = Extract<number | string | boolean, string>;

// 1 단계
// Extract<number, string> |
// Extract<string, string> |
// Extract<boolean, string>


// 2 단계
// never |
// string |
// never

// 최종 결과
// string

분산적인 조건부 타입을 막고 싶다면
[T] extends [number] ? string : number
// 이렇게 하면 분산적으로 가지 않는다.

타입 추론 with infer

조건부 타입 내에서 특정 타입만 추론해낼 수 있는 기능

type Func = () => string;
type FuncB = () => number;

type ReturnType<T> = T extends () => string ? string : never

type A = ReturnType<Func>; // string
type B = ReturnType<FuncB>; // never


// infer 사용
type ReturnType<T> = T extends () => infer R ? R : never  -> R을 추론해라

() => string 이 infer R 타입에 서브 타입인지 확인하게 된다. 이때 infer R은 funcB의 () => number를 참으로 만들게 끔 동작합니다. 이때 R은 그러면 number로 추론이 된다.


type ReturnType<T> = T extends () => infer R ? R : never;

type A = ReturnType<() => string>; // string
type B = ReturnType<() => number>; // number
type C = ReturnType<number>;       // never (함수가 아님)

예제

type PromiseUnpack<T> = T extends Promise<infer R> ? R : never;

type P1 = PromiseUnpack<Promise<number>>; // number
type P2 = PromiseUnpack<Promise<string>>; // string
  • Promise 형태의 타입에서 Something만 추출하고 싶을 때 사용
개념설명
T extends U ? A : B조건에 따라 타입 분기
분산 조건부 타입유니온 타입에 대해 분기 조건 개별 적용됨
Exclude<T, U>T에서 U를 제거
Extract<T, U>T에서 U만 추출
infer조건부 타입 내에서 특정 타입을 추론해서 사용
[T] extends [U]분산 조건부 타입 방지 패턴

Day 1️⃣4️⃣

TypeScript 내장 유틸리티 타입들의 기능과 원리(직접 구현)를 총정리한 날입니다.

🧰 유틸리티 타입이란?

기존 타입을 변형하거나 조합해 새로운 타입을 만들어내는 도구

✅ 맵드 타입 기반

  • Partial <T>
  • Required <T>
  • Readonly <T>
  • Pick <T, K>
  • Omit <T, K>
  • Record <K, V>

✅ 조건부 타입 기반

  • Exclude <T, U>
  • Extract <T, U>
  • ReturnType <T>

🔹 Partial <T>

부분적인, 일부분의 라는 의미를 가지고 있습니다.

특정 객체 타입의 모든 프로퍼티를 선택적 프롭퍼티 바꿔주는 타입

interface Post {
  title: string;
  content: string;
  tags: string[];
  thumbnailUrl: string;
}

const draft: Partial<Post> = {
  title: "임시 제목",
  content: "초안입니다.",
};

type Partial<<T> = {
    // 맵드 타입
    [key in keyof T]?: T[key] // 인덱스드 엑세스타입
}

🔹 Required <T>

"필수의", "필수적인" 이라는 의미를 가지고 있습니다.

특정 객체 타입의 모든 프로퍼티를 필수 프로퍼티로 바꿔주는 타입

// Required 직접 구현
type Required<T> = {
  [K in keyof T]-?: T[K];
};

const completePost: Required<Post> = {
  title: "완성된 글",
  content: "내용",
  tags: ["ts"],
  thumbnailUrl: "https://...",
};

Readonly <T> - 읽기 전용 수정불가

특정 객체 타입에서 모든 프로퍼티를 읽기 전용 프로퍼티로 만들어주는 타입

// 직접 구현
type Readonly<T> = {
  readonly [K in keyof T]: T[K];
};

const readonlyPost: Readonly<Post> = {
  title: "읽기 전용 글",
  content: "수정 불가",
  tags: [],
  thumbnailUrl: "",
};

Pick <T, K> -> 뽑다, 고르다

객체 타입으로부터 특정 프로퍼티만 딱 골라내는 그런 타입

interface Post {
    title:String;
    tags: string[];
    content: string;
    thumbnailUrl?: string;
}

// 직접 구현
type Pick<T,K extends keyof T> = {
    [key in K] : T[key]
}

const legacyPost: Pick<Post,"title"|"content"> = {
    title:"엣날 글",
    content: "옛날 컨텐츠",
}

K에는 제약을 걸어놓아야 한다. 그렇지 안흥면 함수타입 number 타입 등 여러 타입들이 들어올 수 있게 됩니다.

K에는 반드시 T의 key만 올 수 있도록 extends keyof T로 제약을 걸어야 합니다.

K는 T값에서 추출한 유니온 타입인 서브타입만 들어올 수 있게 때문이다.

풀어서 설명해보자.
T에 Post에 들어가게 된다면

  • K extends 'title' | 'tags' | 'content' | 'thumbnailURL'
  • 'title' | 'content' extends 'title' | 'tags' | 'content' | 'thumbnailURL'

(extends 기준으로) 좌측은 우측의 서브타입이기 때문에 가능하다. 그리고 K에는 T 객체 프로퍼티의 키만 전달할 수 있도록 해야한다.

Omit <T,K> -> 생략하다, 빼다

객체 타입으로부터 특정 프로퍼티를 제거하는 타입

// 직접 구현하기
type Omit<T, K exnteds typeof T> = Pick<T, Exclude<keyof T, K>>;
// 풀어서 적어보기
Pick<Post, Exclude<keyof Post, 'title'>>
Pick<Post, Exclude<keyof 'title' | 'tags' | 'content' | 'thumbnailURL', 'title'>>
Pick<Post, 'content' | 'tags' | 'thumbnailURL'>>

const noTitlePost: Omit<Post, "title"> = {
  content: "제목 없음",
  tags: [],
  thumbnailUrl: "",
};

Record <K, V>

K를 key로, V를 value로 갖는 객체 타입 생성

type ThumbnailLegacy = {
    large: {
        url: string;
    };
    medium: {
        url: string;
    };
    small: {
        url: string;
    }
    watch: {
        url: string
    }
}

type Thumbnail = Record<"large"| "mediumm" | "small" | "watch", { url:string }>;
// 직접 구현
type Thumbnail = Record<K extends keyof any, V> = {
    [key in K]: V;
}

Exclude <T, U> -> 제외하다, 추방하다

T에서 U를 제거하는 타입

type A = Exclude<string | boolean, boolean> => string

// 직접 구현
type Exclude<T,U> =  T extends U ? never : T;

Extract <T, U>

T에서 U를 추출합는 타입

// 직접 구현
type Extract<T, U> = T extends U ? T : never

ReturnType

함수의 반환값 타입을 추출하는 타입

function funcA(){
    return "hello";
}

function funcB(){
    return 10;
}

type ReturnA = ReturnType<typeof funcA> // string
type ReturnB = ReturnType<typeof funcB> // number

// 직접 구현
type ReturnType<T extends (...args: any) => any> = T extends (... args : any) => infer R ? R : never;

🌅 타입스크립트 한 입 챌린지 6기 여정…

퇴근 후 짬을 내어 수업을 듣고 한입 챌린지 6기를 완주했다는 사실에 스스로도 뿌듯함을 느낍니다. (정리하다보니 두서없이 작성한 것 같아 보시는데 불편함이 있으실까 걱정이 됩니다.🥹 )

이번 과정을 통해 실무에서 다른 분들의 타입스크립트 설계를 보며 자극을 받았고,
저 또한 더 효율적이고 유연하게 타입을 설계하는 법에 대해 많이 고민하게 되었습니다.

Day 1️⃣부터 Day 1️⃣4️⃣까지의 내용을 정리하면서, 단순히 문법을 익히는 것을 넘어
실무에서 마주했던 애매한 타입 설계 문제들을 스스로 해석하고 해결할 수 있는 기준이 생겼습니다.

글로 풀어내는 과정 속에서 표면적인 이해를 넘어 개념의 맥락과 의미를 되짚어보는 시간이었습니다.☺️

끝까지 읽어주셔서 감사합니다. 🙇‍♂️

강의: 한 입 크기로 잘라먹는 TypeScript
타입 계층도 이미지 출처: 한 입 크기로 잘라먹는 타입스크립트(TypeScript)

profile
실패를 두려워하지 않고 끊임없이 시도하는 프론트엔드 개발자입니다.

0개의 댓글