-
타입스크립트(ZeroCho) lib,es5,d,ts 분석카테고리 없음 2023. 8. 10. 18:33
forEach, map 제네릭 분석
forEach
interface Array<T>{ forEach(callbackfn:(value: T, index:number,array:T[])=>ConstrainDOMString, thisArg?:any):void; } [1,2,3].forEach((item)=>{console.log(item);})
1. 자바스크립트로만 생각했을때 아래 코드는 짐작해봤을때 콘솔에 1,2,3 이 찍힌다.
item이 number로 추론되어있다.
이처럼 string number, boolean을 다추론이 정상적으로 된다.
이렇게 자동으로 추론을 잘 하는 이유는 위 코드에서
value = item
array:T[] = [true,false,true]
interface Array<T>{ forEach(callbackfn:(value: T, index:number,array:T[])=>ConstrainDOMString, thisArg?:any):void; } const a: Array<number> = [1,2,3]; a.forEach((value)=>{console.log(value)}) ['1','2','3'].forEach((value)=>{console.log(value);}) [true,false,true].forEach((value)=>{console.log(value);})
처음 Array<T>가 number로 타입이 정의되면 이후 T들도 number가 되고 string이면 나머지 T들도 string이된다.
코드를 작성할때는 어떤 타입이 올지 모르지만 실행이되면 어떤게 들어올지 알때 제네릭을 사용한다.
제네릭을 사용하지않으면
function add(x:string|number, y:string|number){} add('1',2) add(1,'2')
이렇게 문자열과 숫자열이 같이 나와있는걸 막을 수 없다.
그래서 제네릭을 사용해서 같은 타입끼리만 가능하게 할 수 있다.
function add<T>(x:T, y:T){retrun x} add('1','2') add(1,2) add(true,false)
타입스크립트는 추론을 x, y중 타입이 있는지 확인하고 T가 number가 되면 나머지도 number로 추론한다.
직접 타입을 지정해주는건 타입스크립트가 제대로 타입을 지정해주지 못할때 사용한다.
map
interface Array<T> { map<U>(callbackfn:(value:T,index:number, array:T[])=>U, thisArg?:any):U[] } const strings = [1,2,3].map((value)=>value+1); // [2,3,4] number[]
filter 제네릭 분석
interface Array<T> { filter<S extends T>(predicate: (value: T, index: number, array: T[])=>value is S, thisArg?:any):S[]; } const filtered = [1,2,3,4,5].filter((value)=>value % 2); ⬇️ filter<number extends number>(predicate: (value: number, index: number, array: number[])=>value is number, thisArg?:any):number[];
filtered는 number로 추론된다. 고로 T는 number가 된다.
제네릭 분석은 찾기다 왜 filtered가 number인지 T를 진짜 타입으로 찾아가면 풀린다.
string인 것만 필터하고싶은데 filtered의 타입추론이 string | number로 나와서 string인것만 분리할 수 없다.
이럴경우 왜 추론을 못하는지? 추론을 잘 하게하려면 어떻게 해야하는지 생각해봐야한다.
filter<S extends string | number>(predicate: (value: T, index: number, array: T[])=>value is S, thisArg?:any):string | number[];
결국에는 string | number [] 로 되기때문에 추론도 string | number가 되는거다.
해결방법
interface Array<T> { filter<S extends T>(predicate: (value: T, index: number, array: T[])=>value is S, thisArg?:any):S[]; } // S를 원하는 타입으로 바꿔서 predicate를 똑같이 만들어준다. const predicate = (value: string | number): value is string => typeof value === 'string'; const filtered = ['1',2,'3',4,'5'].filter(predicate);
이렇게 해주면 filtered의 타입추론은 string[]으로 나온다.
forEach 타입 직접 만들기
1. interface를 만든다.
2. forEach 메서드 선언을 하고 리턴을 사용안한다면 void로 처리한다.
// 1. interface를 만든다. interface Arr { // 2. forEach 메서드 선언을 하고 리턴값 정해진게 없다면 void로 처리한다. forEach():void }
3. 콜백 자리를 만든다.
4. item의 타입 정의를 해준다.
// 1. interface를 만든다. interface Arr { // 2. forEach 메서드 선언을 하고 리턴값을 사용안한다면 void로 처리한다. // 3. 콜백 자리를 만든다. // 4. item의 타입 정의를 해준다. 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'; })
이렇게 하면 에러는 사라진다 그치만 forEach함수는 여러가지가 들어올 수 있어서 b를 만들어보면 에러가 난다..
에러는 string형식은 number형식에 할당할 수 없다고 나온다.
interface Arr { forEach(callback:(item:string)=> void ):void }
item을 string으로 변경을하면 b의 에러는 사라지지만 이번엔 a에 에러가 발생한다.
interface Arr { forEach(callback:(item:string | number)=> void ):void }
이렇게 해주면 에러가 발생하지 않는다. 다만 이렇게 하면 조금만 복잡해져도 에러가 발생할 수 있다.
interface Arr<T> { forEach(callback:(item:T)=> void ):void } const a: Arr<number> = [1,2,3]; a.forEach((item)=>{ console.log(item); }); 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'; })
제네릭을 넣어서 사용하면 에러를 없애고 타입을 지정할 수 있다.
map 타입 직접 만들기
interface Arr<T> { forEach(callback:(item:T)=> void ):void } const a: Arr<number> = [1,2,3]; const b = a.map((v)=> v+1)
b의 타입을 추론한다.
1. 함수를 만든다.
interface Arr<T> { map():void }
2. 콜백함수를 넣는다.
interface Arr<T> { map(callback:(v)=> void):void }
3. 제네릭을 추가한다.
interface Arr<T> { map(callback:(v: T)=> T):T[]; } const a: Arr<number> = [1,2,3]; const b = a.map((v)=> v+1)
4. 여러 조건을 만족하게 수정한다.
interface Arr<T> { map<S>(callback:(v: T)=> S):S[]; } const a: Arr<number> = [1,2,3]; const b = a.map((v)=> v+1); const c = a.map((v)=>v.toString());
toString의 문자 타입을 만족하기 위해 S를 추가한다.
interface Arr<T> { map<S>(callback:(v: T)=> S):S[]; } const a: Arr<number> = [1,2,3]; const b = a.map((v)=> v+1); const c = a.map((v)=> v.toString()); const d = a.map((v)=> v % 2 === 0);
만약 인덱스를 추가하게된다면
interface Arr<T> { map<S>(callback:(v: T, i: number)=> S):S[]; } const a: Arr<number> = [1,2,3]; const b = a.map((v, i)=> v+1); const c = a.map((v)=> v.toString()); const d = a.map((v)=> v % 2 === 0);
그때그때 추가해서 수정해주면 된다.
여러 조건을 만족하는 타입의 map
interface Arr<T> { map<S>(callback:(v: T, i: number)=> S):S[]; } const a: Arr<number> = [1,2,3]; const b = a.map((v, i)=> v+1); const c = a.map((v)=> v.toString()); const d = a.map((v)=> v % 2 === 0); const e: Arr<string> = ['1','2','3']; const f= e.map((v)=> +v)
filter 타입 직접 만들기
interface Arr<T> { filter(callback:(v: T)=> boolean):T[]; } const a: Arr<number> = [1,2,3]; const b = a.filter((v)=> v % 2 === 0); // [2] number[]
interface Arr<T> { filter(callback:(v: T)=> boolean):T[]; } const a: Arr<number> = [1,2,3]; const b = a.filter((v)=> v % 2 === 0); // [2] number[] const c: Arr<number|string> = [1,'2',3,'4',5]; const d = c.filter((v)=> typeof v === 'string') //['2','4'] string[]
이렇게 되면 b는 타입이 정상적으로 추론되는데
문제는 d가 string| number[]가 되버린다.
interface Arr<T> { filter<S>(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[]
타입가드를 사용하면서 결과값이 다 T가 아닌 S제네릭을 추가하면 d의 타입추론이 string[]으로 바뀐걸 볼 수 있다.
그치만 T는 S일 수 없다고 에러가 뜬다.
넓은 타입을 좁은타입으로 줄여줄수 있는데 그럴때 <S extends T>를 사용해서 S는 T의 부분집합이라는걸 정의해서 사용할 수 있다.
interface Arr<T> { filter<S extends T>(callback:(v: T)=> v is S):S[]; }
const predicate = (v:string|number): v is number => typeof v === 'number' const e = c.filter(predicate)
위 예제처럼 분리해서 사용하면 조금은 가독성이 약간 좋아보일 수 있다.
공변성과 반공변성
function a(x:string): number { return +x; } a('1'); // 1 type B = (x: string) => number | string // 리턴값은 더 넓은 타입으로 대입이 가능하다. const b: B = a;
리턴값은 좁은타입에서 더 넓은 타입으로 대입이 불가능하다. 넓은 타입으로만 대입 가능하다
function a(x:string | number): number { return +x; } type B = (x: string) => number ; // 리턴값은 더 넓은 타입으로 대입이 가능하다. const b: B = a;
매개변수는 리턴값과는 반대로 좁은타입으로 대입이 가능하고 넓은 타입으로는 대입이 불가능하다.
리턴값은 넓은 타입으로 대입이 가능하고 매개변수는 좁은타입으로 대입이 가능하다
오버로딩
같은코드를 여러번 선언하는걸 오버로딩이라 한다.
declare function add(x:number, y:number) :number declare function add(x:number, y:number, z:number) :number add(1,2) add(2,3,4)
add함수호출 두번을 한번에 처리하는걸 모르겠을땐 그냥 각각 써주면된다.
한번에 해결하는 방법으로는 옵셔널을 이용하면된다.
declare function add(x:number, y:number, z?:number) :number add(1,2) add(2,3,4)
타입스크립트는 건망증이 심하다(+에러 처리법)
interface Axios{ get():void } interface CustomError extends Error{ response?:{ data:any; } } declare const axios: Axios try { await axios.get(); } catch (err: unknown) { console.error((err as CustomError).response?.data); //위에서 err가 뭔지 알려줬으나 바로 밑에서 또 err를 적으면 커스텀인지 알수 없다. }
as를 사용 했다면 변수에 담아서 사용하면 코드마다 as를 안넣을 수 있다.
interface Axios{ get():void } interface CustomError extends Error{ response?:{ data:any; } } declare const axios: Axios try { await axios.get(); } catch (err: unknown) { const customError = err as CustomError console.error(customError.response?.data); //위에서 err가 뭔지 알려줬으나 바로 밑에서 또 err를 적으면 커스텀인지 알수 없다. customError.response.data }
as를 사용하는건 최대한 지양해야 한다. 그리고 이렇게 사용하면 실수가 무조건 날 수도 있고 커스텀에러가 아니라 다른에러가 생길 수 도 있기 때문에 조건문을 사용해줘야 한다.
interface Axios{ get():void } class CustomError extends Error{ response?:{ data:any; } } declare const axios: Axios try { await axios.get(); } catch (err) { if(err instanceof CustomError) console.error(err.response?.data) err.response?.data }
as를 사용하지 않는게 제일 좋은 코드고 unknown이 있으면 타입가드를 사용해주는게 좋다!
인터페이스는 자바스크립트에서 사라지기 때문에 유지시키고 싶으면 class를 사용해준다.