목차
3장은 타입스크립트가 제공하는 타입이 모여 있는 lib.es5.d.ts 파일을 분석하는 장이다. 이 장을 통해 타입이 어떻게 선언됐는지 알 수 있다.
.d.ts 파일에는 타입 선언만 있고 실제 구현부는 존재하지 않는다. 이는 자바스크립트 문법은 따로 구현돼 있거 ts에서는 타입선언만 제공하기 때문이다.
이 장에서 타입을 직접 구현해 보며 이해할 수 있을 것같다.
- Utility 타입 알아보기
3.1 Partial, Required, Readonly, Pick, Record
: Partial, Required, Readonly, Pick, Record는 타입 스크립트 공식 사이트의 Reference 중 Utility Types에서 매핑된 객체 타입만 추린 것
- 유틸리티 : 기본적으로 제공되는 타입들을 변형하거나 조합해 새로운 타입을 생성할 수 있도록 해주는 기능
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}>
- 모든 객체가 옵셔널
사용 예 - 인터페이스의 일부 필드만 필요한 상황에서 유용
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: obj 객체가 아니라 data와 methods 객체를 합친 타입?
- this는 obj 전체를 가리킴 (obj 객체 그 자체는 아니지만)
- obj 전체의 타입이나 구조를 참조
- this가 메서드 내부에서 사용될 때, this는 메서드를 호출한 객체를 참조
- this는 obj 구조(타입)를 기반으로 동작
- TS에선 객체 리터널 내부에서 this의 타입을 자동 추론하지 못하는 경우가 존재
- -> this가 명확히 정의되지 않으면, this가 객체 전체를 암시적으로 참조한다 가정
- 하지만, this는 구조나 타입정보만 참고, 실제 obj를 직접 가리키진 X
- this가 obj 전체의 구조를 참조할 때 의미
- TypeScript에서 메서드 정의 시 명시적으로 this의 타입을 설정하지 않으면
- this는 암시적으로 객체 전체의 구조(타입)를 기반으로 동작한다 간주
- 런타임에서는 this가 메서드를 호출한 컨텍스트에 의해 결정
- TypeScript에서 메서드 정의 시 명시적으로 this의 타입을 설정하지 않으면
- this는 obj 전체를 가리킴 (obj 객체 그 자체는 아니지만)
- 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 |