Today I Learned_230314
TypeScript
filter 분석
같은 함수가 여러가지 방법으로 선언되는 경우에는 여러 타입으로 선언되는 경우가 있음
→ filter는 unknown을 받고있어서 타입 추론을 제대로 못 하는 경우도 있다.
제네릭의 경우 한 칸 씩 진짜 타입으로 채워가면서 분석해 나가자
interface Array<T>{
filter<S extends T>(predicate: (value: T, index: number, array: T[]) => value is S, thisArg?: any): S[];
filter(predicate: (value: T, index: number, array: T[]) => unknown, thisArg?: any): T[];
}
// value는 Number
// number를 2로 나눈 나머지 - number이므로 S는 number라고 추론 가능
const filtered = [1, 2, 3, 4, 5].filter((vaule) => vaule % 2)
타입 채워나가면 아래와 같다.
filter<number extends number>(predicate: (value: number, index: number, array: number[]) => value is number, thisArg?: any): number[];
// filter(predicate: (value: string | number, index: number, array: string | number[]) => unknown, thisArg?: any): string | number[];
// 결과가 string | number[] 니까 string | number로 나오게 되는 상황.
const filtered1 = ['1', 2, '3', 4, '5'].filter((value) => typeof value === 'string')// ['1','3', '5'] string[]
// string extends string | number
constpredicate= (value: string|number): value is string => typeof value === 'string'
const filtered2 = ['1', 2, '3', 4, '5'].filter(predicate);
// 아래꺼는 에러가 난다.
// <string extends string | number> -> 직접 기입. T는 정해져 있기 때문에 extends를 붙지 않아도 된다.
// 커스텀 타입 가드가 아니기 때문에 되지 않음
// const result = ['1', 2].filter<string extends string | number>((value) => typeof value === 'string');
//(value) => typeof value === 'string' 이게 형식 조건자에 맞지 않는 상황.
const result = ['1', 2].filter((value) => typeof value === 'string');
- 결과는 string[]이어야 하는데 filtered1는 string|number가 된다
- 추론을 잘못한 것. 추론을 잘 하도록 해야한다.
- string[]으로 나오게 하려면 이에 맞게 바꾸어 줘야 한다.
- predicate를 별도로 빼자. 이 순간 S는 string이라고 알려준다.
forEach 타입 만들기
타입을 제대로 만들 줄도 알아야 함.
a.forEach((item) => {
console.log(item);
});
a.forEach((item) => {
console.log(item);
return '3';
})
위 형태를 바탕으로 forEach 함수 구현
인터페이스 선언 → 함수 선언 → callback 추가
에러 찾아가는 방법 : 마지막 줄 부터 보고 천천히 위로 올라가면서 찾아나가자.
interface Arr {
forEach (callback: (item: number) => void) : void; // 모든 타입을 커버할 수 없음
}
// --------여기는 가능
const a : Arr = [1, 2, 3];
a.forEach((item) => {
console.log(item);
});
a.forEach((item) => {
console.log(item);
return '3';
})
// --------여기는 불가능
const b : Arr = ['1', '2', '3']
b.forEach((item) => {
console.log(item);
});
b.forEach((item) => {
console.log(item);
return '3';
})
interface Arr {
forEach (callback: (item: string | number) => void) : void;
}
const a : Arr = [1, 2, 3];
a.forEach((item) => {
console.log(item);
item.toFixed(1); //가능해야 하는데 현재 구조로는 불가
});
a.forEach((item) => {
console.log(item);
return '3';
})
const b : Arr = ['1', '2', '3']
b.forEach((item) => {
console.log(item);
item.charAt(3); //가능해야 하는데 현재 구조로는 불가
});
b.forEach((item) => {
console.log(item);
return '3';
})
일단 에러가 나오지 않으면 성공한것. 근데 복잡하게 만들다 보면 나중에 가서야 잘못된 것을 발견하게 되는 경우도 있음..
또는 관계로 표현이 되지 않음 → 제네릭 사용
interface Arr {
forEach<T> (callback: (item: T) => void) : void;
}
이러면 제네릭 자리를 추론을 하지 못해 아래와 같이 명시해주어야 함
a.forEach<number>((item) => {
console.log(item);
item.toFixed(1);
});
아래와 같이 수정 진행
interface Arr<T> {
forEach (callback: (item: T) => void) : void;
}
const a : Arr<number> = [1, 2, 3];
a.forEach((item) => {
console.log(item);
item.toFixed(1);
});
a.forEach((item) => {
console.log(item);
return '3';
})
const b : Arr<string> = ['1', '2', '3']
b.forEach((item) => {
console.log(item);
item.charAt(3);
});
b.forEach((item) => {
console.log(item);
return '3';
})
원본 forEach
forEach(callbackfn: (value: number, index: number, array: Int8Array) => void, thisArg?: any): void;
구현부에서는 index를 사용하고 있지 않으므로 생략.
실제 구현 코드 발전도에 따라 타입을 발전해 나가자. 처음부터 모든 것을 예상하려 하지 말자.
map 타입 만들기
interface Arr<T> {
map (callback: (v: T) => T) : T[];
}
const a : Arr<number> = [1, 2, 3];
const b = a.map((v) => v + 1); // [2, 3, 4]
그런데 위와 같이 하면 아래 경우가 안 된다.
const c = a.map((v) => v.toString()); // ['2', '3', '4']
v.toString에서 TS2322: Type ‘string’ is not assignable to type ‘number’. 발생
또한 리턴값이 string[]이 되어야 하는데 c의 타입을 보면 number[]로 되어 있음
새로운 제네릭 문자를 추가하면 된다.
interface Arr<T> {
map<S> (callback: (v: T) => S) : S[];
}
const a : Arr<number> = [1, 2, 3];
const b = a.map((v) => v + 1); // [2, 3, 4]
const c = a.map((v) => v.toString()); // ['2', '3', '4'], string[]
const d = a.map((v) => v % 2 === 0); //[false, true, false], boolean[]
const e: Arr<string> = ['1' ,'2', '3'];
const f = e.map((v) => +v);
filter 타입 만들기
interface Arr<T> {
filter (callback: (v: T) => v is T) : T[];
}
const a : Arr<number> = [1, 2, 3];
const b = a.filter((v) : v is number => v % 2 === 0); // [2], number[]
const c : Arr<number | string> = [1, '2', 3, '4', 5];
const d = c.filter((v) : v is string => typeof v === 'string'); // ['2', '4'], string[]
커스텀 타입 가드를 사용해도 d는 여전히 number|string형식
T타입이 S타입으로 변화하려면 extends를 써서 확장 (부분집합 관계이므로)
커스텀 타입 가드(형식 조건자)는 넓은 타입을 좁은 타입으로 좁혀주는 역할을 한다.
interface Arr<T> {
filter<S extends T> (callback: (v: T) => v is S) : S[];
}
const a : Arr<number> = [1, 2, 3];
const b = a.filter((v) : v is number => v % 2 === 0); // [2], number[]
const c : Arr<number | string> = [1, '2', 3, '4', 5];
const d = c.filter((v) : v is string => typeof v === 'string'); // ['2', '4'], string[]
interface Arr<T> {
filter<S extends T> (callback: (v: T) => v is S) : S[];
}
const a : Arr<number> = [1, 2, 3];
const b = a.filter((v) : v is number => v % 2 === 0); // [2], number[]
const c : Arr<number | string> = [1, '2', 3, '4', 5];
const d = c.filter((v) : v is string => typeof v === 'string'); // ['2', '4'], string[]
//가독성을 높이려면 바깥으로 뺌.
//그런데 타입스크립트는 인라인으로 쓰나 바깥으로 쓰나 가독성이 안 좋긴 하다..
const predicate = (v : string | number) : v is number => typeof v === 'number';
const e = c.filter(predicate); // [1, 3, 5] number[];
lib.es5.d.ts를 안 보고 만들어내는 연습을 할 수 있으면 좋다.