typescript

lib.es5.d.ts 분석하기 - 3.1 ~ 3.6

케케_ 2024. 11. 3. 16:09

목차

 

3장은 타입스크립트가 제공하는 타입이 모여 있는 lib.es5.d.ts 파일을 분석하는 장이다. 이 장을 통해 타입이 어떻게 선언됐는지 알 수 있다. 

.d.ts 파일에는 타입 선언만 있고 실제 구현부는 존재하지 않는다. 이는 자바스크립트 문법은 따로 구현돼 있거 ts에서는 타입선언만 제공하기 때문이다.

이 장에서 타입을 직접 구현해 보며 이해할 수 있을 것같다.

 

- Utility 타입 알아보기

 

3.1 Partial, Required, Readonly, Pick, Record

: Partial, Required, Readonly, Pick, Record는 타입 스크립트 공식 사이트의 Reference 중 Utility Types에서 매핑된 객체 타입만 추린 것

- 유틸리티 : 기본적으로 제공되는 타입들을 변형하거나 조합새로운 타입을 생성할 수 있도록 해주는 기능

 

definition이 아닌 Reference로
함수 확인

 

Partial

: 부분적인, 불완전한

: 객테 타입의 모든 속성을 옵셔널로 

 

- lib.es5.d.ts 안에 정의 된 partial

/**
 * Make all properties in T optional
 */
type Partial<T> = {
    [P in keyof T]?: T[P];
};

 

- 직접 타입 정의

type MyPartial<T> = {
    [P in keyof T]?: T[P];
};

type Result = MyPartial<{a : string, b : number}>

이름 앞에 My를 붙인 이유

  • 모든 객체가 옵셔널

 

사용 예 - 인터페이스의 일부 필드만 필요한 상황에서 유용

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

// user 객체의 일부 프로퍼티만 포함해도 됨
const updateUser = (user: Partial<User>) => {
    console.log(user.name);  //"Gayeong" 
    console.log(user.id);  //undefined  
};

updateUser({ name: "Gayeong" });

 

 

Required

: partial과 반대로 모든 속성이 필수

type MyRequired<T> = {
    [P in keyof T]-?: T[P];
};

//옵셔널을 붙여 전달해도 옵셔널 제거
type Result = MyRequired<{a? :string, b? :number}>

옵셔널 제거

사용 예

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

const user: Required<User> = { id: 1 }; // (에러)모든 필드가 필수

 

 

Readonly<T>

: 모든 속성 읽기 전용으로 만들기

: 또는 아니게 만들기

 

  • 읽기 전용으로 만들기
type MyReadonly<T> = {
    readonly [P in keyof T]: T[P];
};
type Result = MyReadonly<{a :string, b :number}>

 

 

  • 모든 속성 readonly 제거하기
    • 앞에 -
type MyReadonly<T> = {
    -readonly [P in keyof T]: T[P];
};
type Result = MyReadonly<{a :string, b :number}>

 

 

Pick<T>

: 객체에서 지정한 속성만 선택해 새로운 타입 생성

type MyPick<T, K extends keyof T> = {
    [P in K]: T[P];
};

type Result = MyPick<{a :string, b :number, c: number}, 'a' |'c'>

  • T: { a: string; b: number; c: number}
  • K: 'a' | 'c
    • T의 키 중 일부를 나타내는 유니온 타입 (일부가 아니면 에러)
    • keyof T : T의 키들("a" | "b" | "c")

 

- K 가 T의 일부가 아니면 에러

type MyPick<T, K extends keyof T> = {
    [P in K]: T[P];
};

type Result = MyPick<{a :string, b :number, c: number}, 'a' |'c'|'d'> //에러

 

  • 위의 경우에서 'd'만 무시하고 T의 속성만 추리는 방법
type MyPick<T, K> = {
    [P in (K extends keyof T ? K : never)]: T[P];
};

type Result = MyPick<{a :string, b :number, c: number}, 'a' |'c'|'d'>   //a ,c 로 추려짐

 

-> 단점 : 아래와 같이 K로 'd'만 있는 경우 Result가 {} 타입이 됨

type MyPick<T, K> = {
    [P in (K extends keyof T ? K : never)]: T[P];
};

type Result = MyPick<{a :string, b :number, c: number}, 'd'>   //{}
const result : Result= {a : '에러 발생 안해'}
  • {} :  null, undefined를 제외한 모든 값

- 사용 예

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

const user: Pick<User, "id" | "name"> = { id: 1, name: "Gayeong" }; // id와 name만 포함

 

 

Record<T> 

: 모든 속성의 타입이 동일한 객체의 타입

 

type MyRecord<K extends keyof any, T> = {
    [P in K]: T;
};
type Result = MyRecord<'a' | 'b',string>

  • K keyof any: K에 string | number | symbol 로 제약 
    • JavaScript와 TypeScript에서 객체의 키가 될 수 있는 타입이 위 세가지로 한정되기 때문

 

- 사용 예제

const userAges: Record<string, number> = {
    Gayeong: 25,
    John: 30
};

 객체의 키가 항상 string이고 값은 number 타입

 

 

 

3.2 Exclude, Extract, Omit, NonNullable

: 분배법칙을 확용하는 타입

- 컨디셔널 타입(조건부)과 유니언 타입이 만나면 분배법칙 실행

 

 

Exclude<T, U>

: 어떤 타입에서 지정한 타입을 제거

: T에서 U에 해당하는 타입 제거

 

type MyExclude<T, U> = T extends U ? never : T;

type Result = MyExclude<1|'2'|3, string>;  // 타입 : 1 |3
  • 1|'2'|3 : 유니온 -> 분배법칙 실행
    • MyExclude<1, string> | MyExclude<'2', string> | MyExclude<3, string>
  • 각각 extends string이 참인지 확인 -> 1 | never | 3

 

 

Extract<T, U>

: Exclude와 반대로 지정한 타입만 추출

: T에서 U 에 할당 가능한 타입만 추출

 

type MyExtract<T, U> = T extends U ? T : never;

type Result = MyExtract<1|'2'|3, string>;  // 타입 : '2'
  • Exclude에서 T와 never의 자리만 바꿔줌

 

 

Omit<T, K>

: 특정 객체에서 지정한 속성 제거

: T 타입에서 K에 해당하는 키를 제거해 새로운 타입 생성

 

type MyOmit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;

type Result = MyOmit<{a : '1', b :2 , c: true}, "a" | "c">;  // = {b :"2"}
  • Pick과 반대인 행동을 하지만, Pick과 Exclude 활용
    • Pick : 지정한 속성만 선택
  • Exclude<keyof T, K> : 지정한 속성 제거
    • "a" , "c" 제거 -> "b"만 남음
  • Pick -> 추려낸 속성을 선택 => "b" 속성만 있는 타입이

 

 

NonNullable<T>

: T에서 null undefined를 제거

 

type MyNonNullable<T> = T extends null | undefined ? never : T;

type Result = MyNonNullable<string | number | null | undefined>;  // string | number
  • 분배법칙 실행 후 조건문을 만나 null과 undefined가 제거

 

최신 버전 -> 더 간단히 변경됨

type NonNullable<T> = T & {};
  • {} : null과 undefined를 포함 X
  • T = string | number | null | undefined인 경우 
    • T와 {}의 교집합만 남음 -> string | number

 

 

- 일부 속성만 옵셔널로 만드는 타입 만들어보기

type Optional <T , K extends keyof T >= Omit<T,K> & Partial<Pick<T,K>>

type Result = Optional<{a:'hi', b:123}, 'a'> // {a? : 'hi', b:123}
  • (Pick : 옵셔널이 될 속성 선택) -> (Partial : 들어온 타입을 모두 옵셔널로)
  • Omit : 옵셔널이 되지 않을 속성 추출 (예: 'a'를 제외한 속성 추출)
  • & 연산자로 합침

 

 

3.3 Parameters,ConstructorParameters, ReturnType, InstanceType

: infer를 활용한 타입 알아보기 

 

infer : 조건부 타입과 함께 사용되어 특정 타입을 추론할 때 활용

- 함수와 반환 타입, 튜플, 배열, 객체의 내부 타입을 추출하는데 유용

- 2.22절 

 

type MyParameters<T extends (...args: any[]) => any> 
    = T extends (...args: infer P) => any ? P : never;

type MyConstructorParameters<T extends abstract new (...args: any) => any> 
    = T extends abstract new (...args: infer P) => any ? P : never;

type MyReturnType<T extends (...args: any[]) => any> 
    = T extends (...args: any[]) => infer R ? R : any;

type MyInstanceType<T extends abstract new (...args: any) => any> 
    = T extends abstract new (...args: any) => infer R ? R : any;
  • Parameters<T> : 함수 T의 매개변수 타입을 튜플로 추출
  • ConstructorParameters<T> : 생성자 함수 T의 매개변수 타입을 튜플로 추출
  • ReturnType<T> : 함수 T의 반환 타입을 추출
  • InstanceType<T> : 생성자 함수 T로 생성된 인스턴스의 타입을 추출
  • abstract new (...args: any) => any
    • new (...args: any)=> any : 모든 생성자 함수 (클래스 포함 / 추상 클래스는 포함 X)
    • abstract :  추상 클래스까지 포함하게 해줌

 

Parameters<T> : 함수 T의 매개변수 타입을 튜플로 추출

function logMessage(message: string, level: number) {}

type Params = Parameters<typeof logMessage>;  // [name: string, level: number]
  • typeof 함수이름 : 함수의 전체 타입(매개변수와 반환 타입 포함)
  • [name: string, level: number]
    • 튜플 타입  / 각 요소에 명명된 매개변수 이름이 추가로 표시
    • 명명된 튜플 타입 : 사용하여 튜플이 각각 어떤 역할을 하는지 전달 가능

 

 

ConstructorParameters<T> : 생성자 함수 T의 매개변수 타입을 튜플로 추출

class User {
    constructor(public name: string, public age: number) {}
}

type UserConstructorParams = ConstructorParameters<typeof User>;  // [name : string, age : number]

 

 

 

ReturnType<T> : 함수 T의 반환 타입을 추출

function getUser() {
    return { id: 1, name: "Gayeong" };
}

type UserType = ReturnType<typeof getUser>;  // { id: number; name: string; }

 

 

 

InstanceType<T> : 생성자 함수 T로 생성된 인스턴스의 타입을 추출

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

type UserType = InstanceType<typeof User>;  // User
  • typeof 클래스_이름
    • 클래스의 타입 : 생성자 함수 타입 = new 키워드를 사용해 객체를 생성할 수 있는 타입
    • new (name: string, age: number) => User
      • => User : 생성자로부터 반환되는 객체의 타입
  • new (name: string, age: number) => User 를 받은 InstanceType 은 반환타입인 User가 됨

 

 

3.4 ThisType

: 메서드들에 this를 한 방에 주입

: 주로 객체 리터럴과 함께 사용되어, this가 특정 타입을 가리키도록 명시할 때 유용 (객체 리터널의 타입을 지정할 때 this 타입을 쓰면 좋음)

: 컴파일러가 this의 타입을 미리 알 수 있도록 명시적으로 설정하는 타입

 

(에러 코드)

- 메서드 안에서 this를 쓰고 싶은 상황

const obj = {
    data : {
        money : 0,
    },
    method : {
        addMoney (amount : number) {
            this.money += amount;       //에러 - 속성 money가 존재하지 않음
        },
        useMoney (amount : number) {
            this.money -= amount;       //에러
        }
    }
}

this는 자신을 호출한 method를 참조

  • this:  obj 객체가 아니라 data와 methods 객체를 합친 타입?
    • this는 obj 전체를 가리킴 (obj 객체 그 자체는 아니지만)
      • obj 전체의 타입이나 구조를 참조
      • this가 메서드 내부에서 사용될 때, this는 메서드를 호출한 객체를 참조
    • this는 obj 구조(타입)를 기반으로 동작
      • TS에선 객체 리터널 내부에서 this의 타입을 자동 추론하지 못하는 경우가 존재
      • -> this가 명확히 정의되지 않으면, this가 객체 전체를 암시적으로 참조한다 가정
        • 하지만, this는 구조나 타입정보만 참고, 실제 obj를 직접 가리키진 X
    • this가 obj 전체의 구조를 참조할 때 의미
      • TypeScript에서 메서드 정의 시 명시적으로 this의 타입을 설정하지 않으면
        • this는 암시적으로 객체 전체의 구조(타입)를 기반으로 동작한다 간주
        • 런타임에서는 this가 메서드를 호출한 컨텍스트에 의해 결정
  • this obj의 차이
    • this의 타입: TypeScript에서 this 현재 메서드를 포함한 객체를 참조한다고 가정하므로 obj의 구조를 기반으로 타입을 추론
    • this의 값: 런타임에서 this는 메서드가 어디서 호출되었는지에 따라 동적으로 결정

 

 

- this가 실제 객체를 참조하지 않은 경우

const obj = {
    data: { money: 0 },
    method: {
        addMoney(amount: number) {
            console.log(this);  // 'method' 객체가 아닌 {} / 엄격모드시 undefined
        }
    }
};

obj.method.addMoney(10);
  • this의 실제 값은 method 객체 undefined가 될 수 있음
  • obj 자체를 가리키지는 않음: this obj 전체를 직접 참조하는 것이 아니라, 특정 호출 컨텍스트에 의해 결정

 

- obj 전체를 가리킨다고 표현하는 이유

const obj = {
    data: {
        money: 0,
    },
    method: {
        addMoney(amount: number) {
            this.data.money += amount;  // 에러: 'this'는 'obj' 전체를 가리키는 것으로 간주되지만, 명확하지 않음
        }
    }
};
  • this를 obj 전체로 간주하기 때문에, this.data.money에 접근하려고 하면 오류가 발생
  • TypeScript는 메서드가 method 객체 내부에서 정의되었더라도 this 명확히 어떤 부분을 참조하는지 이해하지 못함

 

(위에 코드 해결해보기)

- this.data.money 가 아닌 this.money로 접근하고 싶음

- this.method.addMoney 가 아닌 this.addMoney로 접근하고 싶음

type Data = {money : number};
type Methods = {
    addMoney ( this : Data & Methods, amount : number) : void;
    useMoney ( this : Data & Methods, amount : number) : void;     
};
type Obj = {
    data: Data;
    method : Methods;
};

const obj : Obj = {
    data : {
        money : 0,
    },
    method : {
        addMoney (amount : number) {
            this.money += amount;     
        },
        useMoney (amount : number) {
            this.money -= amount;    
        }
    }
}
  • 메서드에 this를 직접 타이핑해 해결
    • 하지만, 앞으로 추가될 모든 메서드에 this를 일일이 타이핑 -> 중복 발생 (this : Data & Methods)

 

ThisType으로 해결

type Data = {money : number};
type Methods = {
    addMoney ( amount : number) : void;
    useMoney ( amount : number) : void;     
};
type Obj = {
    data: Data;
    method : Methods & ThisType<Data & Methods>;
};

const obj : Obj = {
    data : {
        money : 0,
    },
    method : {
        addMoney (amount : number) {
            this.money += amount;     
        },
        useMoney (amount : number) {
            this.money -= amount;    
        }
    }
}
  • this는 Data & Methods 가 됨

 

-ThisType 타입은 lib.es5.d.ts 에 구현 X

/**
 * Marker for contextual 'this' type
 */
interface ThisType<T> {}
  • 타입스크립트로 구현할 수 없기 때문

 

 

3.5 forEach 만들기

: 배열의 메서드 직접 타이핑해보기

//lib.es5.d.ts
forEach(callbackfn: (value: T, index: number, array: T[]) => void, thisArg?: any): void;

 

 

1. myforEach 메서드 만들기 

[1,2,3].myforEach(()=>{})
//에러
Property 'myforEach' does not exist on type 'number[]'. Did you mean 'forEach'?

 

-> lib.es5.d.ts 는 Array를 인터페이스로 만들어 뒀기 때문

-> 같은 이름의 인터페이스를 만들어 병합

 

2. 인터페이스 병합

[1,2,3].myforEach(()=>{}) //Expected 0 arguments, but got 1.


interface Array<T> {
    myforEach() : void;
}
  • lib.es5.d.ts 안 Array 선언에 맞춰 <T>까지 챙겨줘야 함

 

3. 인수를 넣을 수 잇게 매개변수 타이핑

[1,2,3].myforEach(()=>{}) 


interface Array<T> {
    myforEach(callback: () => void) : void;
}
  • callback: 인자로 전달되는 함수 / 반환타입 void
    • 각 배열 요소에 대해 실행

 

(테스트)

[1,2,3].myforEach(()=>{});
[1,2,3].myforEach((v, i, a)=>{console.log(v, i, a)});  //(에러)Argument of type '(v: any, i: any, a: any) => void' is not assignable to parameter of type '() => void'.
[1,2,3].myforEach((v, i)=>{console.log(v)});           //(에러)Argument of type '(v: any, i: any) => void' is not assignable to parameter of type '() => void'.
[1,2,3].myforEach((v)=>3);                             //(에러)Parameter 'v' implicitly has an 'any' type.
                                                       //(에러)Type 'number' is not assignable to type 'void'.


interface Array<T> {
    myforEach(callback: () => void) : void;
}

 

4. 매개변수 타이핑 -> 에러 해결

- forEach 메서드의 콜백함수 : 매개변수가 3개 (요소_값, 인덱스, 원본_배열) 순서

[1,2,3].myforEach(()=>{});
[1,2,3].myforEach((v, i, a)=>{console.log(v, i, a)});  
[1,2,3].myforEach((v, i)=>{console.log(v)});          
[1,2,3].myforEach((v)=>3);                          

interface Array<T> {
    myforEach(callback: (v: number, i:number, a: number[]) => void) : void;
}

 

(테스트)

[1,2,3].myforEach(()=>{});
[1,2,3].myforEach((v, i, a)=>{console.log(v, i, a)});  
[1,2,3].myforEach((v, i)=>{console.log(v)});          
[1,2,3].myforEach((v)=>3);      
['1','2','3'].myforEach((v)=>{
    console.log(v.slice(0))             //(에러)Property 'slice' does not exist on type 'number'.
});                   
[true, 2, 3].myforEach((v)=>{
    if(typeof v === 'string') {
        v.slice(0);                     //(에러)Property 'slice' does not exist on type 'never'.
    } else {
        v.toFixed();					//에러가 발생해야 맞는데, 발생하지 않음
    }
});       


interface Array<T> {
    myforEach(callback: (v: number, i:number, a: number[]) => void) : void;
}
  • 원인 : 각각 요소와 원본 배열의 타입인 매개변수 v와 a가 모두 number기반으로 고정됨
  • T : Array의 제네릭 타입 매개변수
    • 요소의 타입을 의미

(해결 -1) : 제네릭 기반으로 타입 수정

[1,2,3].myforEach(()=>{});
[1,2,3].myforEach((v, i, a)=>{console.log(v, i, a)});  
[1,2,3].myforEach((v, i)=>{console.log(v)});          
[1,2,3].myforEach((v)=>3);      
['1','2','3'].myforEach((v)=>{
    console.log(v.slice(0))             
});                   
[true, 2, 3].myforEach((v)=>{
    v.toFixed();                //(에러)Property 'toFixed' does not exist on type 'number | boolean'.
                                //  Property 'toFixed' does not exist on type 'false'.
});       


interface Array<T> {
    myforEach(callback: (v: T, i:number, a: T[]) => void) : void;
}
  • toFixed에 에러 나타남

 

-lib.es5.d.ts 확인

//lib.es5.d.ts
forEach(callbackfn: (value: T, index: number, array: T[]) => void, thisArg?: any): void;
  • thisArg : 선택적 인자, 콜백 함수가 호출될 때 this가 참조할 값을 설정
  • thisArg?: any
    • 콜백 함수 선언문에서 this를 사용할 때, this 값을 직접 바꿀 수 있게 하는 부분
    • this 값을 직접 바꿀 수 없으면, 브라우저에서는 this가 window가 되고, Node.js에서는 global이 됨
[1,2,3].forEach(function () {
    console.log(this);  //'this' implicitly has type 'any' because it does not have a type annotation.
})

 

-> this에서 에러 발생 / undefined 출력

 

  • thisArg 사용 예
const obj = {
    multiplier: 2,
    multiply(value: number) {
        console.log(value * this.multiplier);
    }
};

const numbers = [1, 2, 3];

// `thisArg`로 `obj`를 전달
numbers.forEach(function(value) {
    this.multiply(value);
}, obj);
// 출력:
// 2
// 4
// 6
//this에 에러 => function(this: typeof obj, value) 해주면 됨

 

 

myforEach에선 this 타이핑이 되게 수정 (에러 발생하지 않게)

[1,2,3].myforEach (function () {
    console.log(this);      //this : Window
});        
[1,2,3].myforEach (function () {
    console.log(this);      //this : {a : string;}
}, {a: 'b'});   


interface Array<T> {
    myforEach<K=Window>(callback: (this: K, v: T, i:number, a: T[]) => void, thisArg? : K) : void;
}
  • 타입 매개변수 K 선언
    • Array<>자리에는 lib.es5.d.ts와 동일해야하므로 안됨
  • K = Window : K는 기본적으로 Window
    • thisArg를 사용하지 안으면 this의 타입은 Window
    • 사용하면 그 값이 this의 타입
  • 정확하진 않음
    • Node.js에선 global이기 때문

 

 

3.6 map 만들기

: map 메서드 타이핑해보기

 

const r1 = [1,2,3].myMap(()=>{});
const r2 = [1,2,3].myMap((v,i,a)=>v);
const r3 = ['1','2','3'].myMap((v)=>parseInt(v));
const r4 = [{num : 1}, {num :2}, {num :3}].myMap(function(v){
    return v.num;
})

interface Array<T> {
    myMap(callback : (v: T, i: number, a : T[]) => void ) : void;
}

 

 

map : foreach와는 다르게 매개변수가 존재함

-> 위 코드를 수정해야함 (현재 void)

 

(수정)

const r1 = [1,2,3].myMap(()=>{});           //void[]
const r2 = [1,2,3].myMap((v,i,a)=>v);       //number[]
const r3 = ['1','2','3'].myMap((v)=>parseInt(v));   //number[]
const r4 = [{num : 1}, {num :2}, {num :3}].myMap(function(v){   //number[]
    return v.num;
})

interface Array<T> {
    myMap<R>(callback : (v: T, i: number, a : T[]) => R ) : R[];
}
  • 반환값이 어떤 타입이 될지 알 수 없음 => 제네릭 타입 매개변수 사용

 

- lib.es5.d.ts 안 map

interface Array<T> { 
	...
	map<U>(callbackfn: (value: T, index: number, array: T[]) => U, thisArg?: any): U[];
	...
}

'typescript' 카테고리의 다른 글

lib.es5.d.ts 분석하기 - 3.7 ~ 3.11  (0) 2024.11.10
2.28 ~ 2.32  (2) 2024.10.20
2.23 ~ 2.27  (0) 2024.10.13
2.18 ~ 2.22  (1) 2024.10.06
타입스크립트 2.13 ~ 2.17  (0) 2024.09.28