관리 메뉴

꿈꾸는 개발자

10장 제네릭 본문

Learning Typescript

10장 제네릭

rickysin 2023. 3. 28. 21:52
  • TS는 제네릭을 사용해 타입 간의 관계를 알아낸다
    • 제네릭 타입 매개변수를 원하는 만큼 선언 가능 ⇒ 타입 매개변수는 구조체의 각 인스턴스에 대해 타입 인수라고 하는 서로 다른 타입을 함께 제공 가능???????
  • 타입 매개변수는 전형적으로 T or U 같은 단일 문자 이름 또는 Key와 Value 같은 파스칼 케이스 이름을 가짐

10.1 제네릭 함수


  • 매개변수<타입별칭>의 형태로 함수를 제네릭으로 만듦 ⇒ 함수 내 매개변수 타입 애너테이션, 반환값 애너테이션으로 사용 가능!
function iden<T>(input:T){
    return input;
}
const num=iden("me")//타입 me
const stringy=iden(123)//타입 123

//화살표 함수 형태의 제네릭(리액트 환경에서 JSX와 충돌이 있음=>13장에서 다룸)
const iden2=<U>(input:U)=>input;
  • 위와 같은 방법으로 any타입을 사용하지 않고, 다른 입력을 수용하고, 타입 안정성을 유지할 수 있음

10.1.1 명시적 제네릭 호출 타입


  • 제네릭 함수: 함수가 호출되는 방식에 따라 타입 인수 유추 ⇒ 위 코드에선 TS 검사기는 iden에 제공되는 인수를 사용해 ⇒ 매개변수의 타입 인수 유추!
function logWrapper<Input>(callback:(input:Input)=>void){
    return (input:Input)=>{
        console.log("Input",input);
        callback(input);
    }
}
//타입 (input:string)=>void
logWrapper((input:string)=>{
    console.log(input.length);
})

//타입:(input:unknown)=>void;
logWrapper((input)=>{
    console.log(input.length);//Error: length does not exists on type unknown
})
  • TS는 매개변수 타입을 모르는 경우 Input이 무엇이 되어야 하는지 알 방법X
  • 명시적 제네릭 타입 인수: unknown으로 설정되는 것을 파하기 위해 ⇒ 일치여부 확인을 위해 제네릭 호출에서 타입 검사 수행
logWrapper<string>((input)=>{
    console.log(input.length);
})
  • 위와 같이 명시할 수 있음 string 타입임을 명시 가능 ⇒ 필요한 경우에만 지정하는 것이 좋음(필요한 경우에 대해선 명시X)

10.1.2 다중 함수 타입 매개변수


  • 임의의 수의 타입 매개변수를 쉼표로 구분해 함수를 정의함
function A<First,Second>(first:First,second:Second){
    return[first,second]as const;
}

let tuple=A(true,"abc"); //value:readonly [boolean,string] 타입
  • 위와 같이 여러 타입 매개변수를 선언하면, 둘 다 명시적 제네릭 타입으로 선언하거나 아니면 둘 다X(한 쪽만 유추X)
function make<Key,Value>(key:Key,value:Value){
    return {key, value};
}

//ok 타입 둘 다 인수 제공X 
make("abc",123); //타입: {key:string,value:number};

//ok 둘 다 타입 제공됨
make<string,number>("abc",123);//타입: {key:string;value:number};
make<"abc",123>("abc",123) //타입: {key:"abc",value:123};

make<string>("abc",123)//한 쪽만 제공X Error 밠애

<aside> 💡 제네릭 구조체에선 두 개 이상의 매개변수 사용X⇒코드 가독 하락

</aside>

10.2 제네릭 인터페이스


  • 인터페이스에도 제네릭 선언 가능
interface Box<T>{
    inside:T
}
let stringyBox:Box<string>={
    inside:"abc",
}

let numberBox: Box<number>={
    inside:123,
}
let incorrectBox:Box<number>={
    inside:false, //당연히 타입 에러 발생!
}
  • TS의 내장 Array method는 제네렉으로 정의됨 ⇒ Array는 타입 매개변수 T를 사용해서 배열 안에 저장된 데이터의 타입을 나타냄!

10.2.1 유추된 제네릭 인터페이스 타입


  • 함수와 same ⇒ 인터페이스의 타입 인수도 사용법에서 유추 가능!
interface LinkedNode<Value>{
    next?:LinkedNode<Value>;
    value:Value;
}
function getLast<Value>(node:LinkedNode<Value>):Value{
    return node.next? getLast(node.next):node.value;
}
let lastDate=getLast({
    value:new Date("09-13-1993")
});
let lastFruit=getLast({
    next:{
        value:"ban",
    },
    value:"apple",
})
let lastMismatch=getLast({
    next:{
        value:123 //여기에서 이미 number로 지정됨!
    }, 
    value:false, //Error boolean => number 불가!
})
  • 인터페이스 타입 매개변수 ⇒ 타입 애너테이션은 항상 상응하는 타입 인수를 제공해야 함! ⇒ 아니면 에러 발생!

10.3 제네릭 클래스


  • 클래스도 멤버에서 사용할 임의의 수의 타입 매개변수 선언 가능 (클래스의 각 인스턴스는 타입 매개변수로 각자 다른 타입 인수 집합을 가짐)
class Sec<Key,Value>{
    key:Key;
    value:Value;

    constructor(key:Key,value:Value){
        this.key=key;
        this.value=value;
    }
    getValue(key:Key):Value|undefined{
        return this.key===key?this.value:undefined;
    }
}
const storage= new Sec(1234,"abcd"); //타입 Sec<number,string>
storage.getValue(1987);//타입 string | undefined;

10.3.1 명시적 제네릭 클래스 타입


  • 인스턴스 생성 시 함수 생성자에 전달된 매개변수의 타입으로 타입 유추 가능하면⇒TS는 타입 유추를 함 (기본값은 unknown)
class Cur<Input>{
    #callback:(input:Input)=>void;

    constructor(callback:(input:Input)=>void){
        this.#callback=(input:Input)=>{
            console.log("Input",input);
            callback(input);
        }
    }

    call(input:Input){
        this.#callback(input);
    }
}
//이 경우 Input=string 타입 
new Cur((input:string)=>{
    console.log(input.length);
})

//타입:unknown이라 에러 발생!
new Cur((input)=>{
    console.log(input.length);
})

//명시적 제네릭 클래스 타입
new Cur<string>((input)=>{
    console.log(input.length);
})

10.3.2 제네릭 클래스 확장


  • 제네릭 클래스는 extends keywords 다음에 오는 기본 클래스에 사용 가능. TS는 기본 클래스에 대한 타입 인수 유추X
class Quote<T>{
    lines:T;
    constructor(lines:T){
        this.lines=lines;
    }
}

class SpokenQ extends Quote<string[]>{//string[]으로 extends 
    speak(){
        console.log(this.lines.join("\\n"));
    }
}

new Quote("the only real failure is the ").lines;//타입 string
new Quote([4,5,6,6,7]).lines //타입 number[];

new SpokenQ([
    "a", 
    "b",
]).lines; //타입 string[];

new SpokenQ([4,5,6,7,7]); //Error: Type 'number' is not assignable to type 'string'
  • 제네릭 파생 클래스는 자체 타입 인수를 기본 클래스에 번갈아 전달 가능(타입 이름 일치 필요X)
class Quote<T>{
    lines:T;
    constructor(lines:T){
        this.lines=lines;
    }
}

class SpokenQ extends Quote<string[]>{
    speak(){
        console.log(this.lines.join("\\n"));
    }
}

new Quote("the only real failure is the ").lines;//타입 string
new Quote([4,5,6,6,7]).lines //타입 number[];

new SpokenQ([
    "a", 
    "b",
]).lines; //타입 string[];

new SpokenQ([4,5,6,7,7]); //Error: 

class AttributeQ<Value> extends Quote<Value>{
    speak:string;
    constructor(value:Value,speak:string){
        super(value);
        this.speak=speak;
    }
}
new AttributeQ("a","b") //Quote<string> 확장하기???

10.3.3 제네릭 인터페이스 구현


interface ActingC<Role>{
    role:Role;
}
class MoviePart implements ActingC<string>{
    role:string;
    speaking:boolean;
    constructor(role:string, speaking:boolean){
        this.role=role;
        this.speaking=speaking;
    }
}

const part=new MoviePart("aaaa",true);
part.role; //타입 string;
class BB implements ActingC<string>{ //string을 줬는데 boolean을 작성해서 오류!
    role:boolean; //Error  Acting<string>인데 role:boolean을 줘서 오류/boolean으로 수정하면 해당 오류는 해결

}

10.3.4 메서드 제네릭


  • 클래스 메서드는 클래스 인스턴스와 별개로 자체 메서드 타입 선언 가능
class A<Key>{
    key:Key;
    constructor(key:Key){
        this.key=key;
    }
    createPair<Value>(value:Value){
        return {key:this.key,value};
    }
}
//타입: A<string>
const factory=new A("role");
factory.createPair(20); //{key:string, value:number};
//타입: {key:string, value:number}
const numberPair=new A("role").createPair(10);
console.log(numberPair); //{key:string, value:number};

//{key:string,value:string}
const stringPair=factory.createPair("stringPair");
  • 핵심: 메서드의 제너릭은 개별적으로 줄 수 있음

10.3.5 정적 클래스 제네릭


class Both<OnInstance>{
    instanceLog(value:OnInstance){
        console.log(value);
        return value;
    }
    static staticLog<OnStatic>(value:OnStatic){
        let fromInstance:OnInstance;//Error:static type cannot reference class type arguments
        console.log(value);
        return value;
    }
}
const logger=new Both<number[]>;

logger.instanceLog([1,2,3]);

//유추된 OnStatic 타입 인수: boolean[];
Both.staticLog([false,true]);

//유추된 OnStatic 타입 인수: string
Both.staticLog<string>("aaaaa");

10.4 제네릭 타입 별칭


  • 타입 별칭에서도 타입 인수를 제네릭으로 만들 수 있다.
type Nullish<T>= T | null | undefined;
  • 제네릭 타입 별칭은 일반적으로 제네릭 함수의 타입을 설명하는 함수와 사용됨!
type CreatesVa<Input,Output>=(input: Input)=>Output;

let creator:CreatesVa<string,number>;
creator=text=>text.length; //사용 가능!
creator=text=>text.toUpperCase(); //Error:type 'string' is not assignable to type "number"
  • 마지막 creator의 경우 출력값이 number타입인데, string타입으로 assign해서 에러가 발생한다.

10.4.1 제네릭 판별된 유니언


4장 객체에서 언급한 판별된 유니언은

type Result<Data>=FailureR | SuccessfulResult<Data>;

interface FailureR{
    error:Error;
    succeeded:false;
}

interface SuccessfulResult<Data>{
    data:Data;
    succeeded:true;
}
function handleRes(result:Result<string>){
    if(result.succeeded){
        //result SuccessfulResult<string>의 타입
        console.log("we did it")
    }else{
        console.error(`${result.error} `);

    }
    result.data; //Property 'data' does not exist on type 'Result<string>'.
    // Property 'data' does not exist on type 'FailureR'.
}
  • result를 반환할 때 result의 succeeded의 값이 true/false인지 무조건 위처럼 확인해야 한다. ⇒ 위처럼 제네릭 타입 + 판별된 타입을 함께 사용하면 Result와 같이 재사용이 가능한 타입 모델링이 가능해진다.

10.5 제네릭 제한자


  • TS는 제네릭 타입 매개변수의 동작을 수정하는 구문도 제공!

10.5.1 제네릭 기본값


interface Quote<T=string>{
    value:T;
}

let explicit: Quote<number>={value:123};

let implicit:Quote={value:"aaaaaa"}; //string이 default

let mismatch:Quote={value:123}//error: Type 'number' is not assignable to type 'string'
interface KeyValuePair<Key, Value=Key>{
    key:Key;
    value:Value;
}
//타입 KeyValuePair<string, number> => 서로 다른 값을 가질 수 있음
let allExplicit:KeyValuePair<string,number>={
    key:"aaa",
    value:10,
}

//타입: KeyValuePair<string, string>
let oneDefault:KeyValuePair<string>={
    key:"aaaa",
    value:"ten",
}

//Error: 적어도 하나의 argument을 줘야한다. 
let firstMissing:KeyValuePair={
    key:"rating",
    value:10,
}
  • Key, Value ⇒ 서로 다른 값을 가질 수 있지만, 기본적으로 동일한 타입 유지됨(명시하지 않으면)
    • Key는 기본값이 없음 여전히 유추 가능/제공되어야 함
    • 주의: 기본 타입 매개변수는 항상 선언 목록 제일 마지막에 와야 한다
function inTheEnd<First,Second,Third=number,F=string>(){}; //ok

function inTheMiddle<F,S=boolean, T=number, F>(){}//Error 발생

10.6 제한된 제네릭 타입


  • 일부 함수는 제한된 타입에서 작동하는 것 외에 제네릭 타입에는 대부분 타입 제공이 가능!
interface WithLength{
    length:number;
}
function logWithLength<T extends WithLength>(input:T){
    console.log(`length: ${input.length}`);
    return input;
}

logWithLength("aaaa") //타입:string;
logWithLength([false,true]); //타입 boolean[];
logWithLength({length:123}) //타입  {length:number};
logWithLength(new Date()); //Error

// Argument of type 'Date' is not assignable to parameter of type 'WithLength'.
//   Property 'length' is missing in type 'Date' but required in type 'WithLength'.
  • 위와 같이 선언을 하면 T 제네릭에 대한 length를 가진 모든 타입이 받아들여짐 ⇒ length:number를 가진 객체까진 허용됨 ⇒ Date와 같은 타입 형태에는 숫자형 length 멤버X 오류가 발생한다.
    • 객체도 length: (다른 타입)일 경우 바로 에러가 발생한다.

10.6.1 keyof와 제한된 타입 매개변수


  • keyof 연산자는 제한된 타입 매개변수와도 잘 동작함
  • extends와 keyof를 함께 사용하면 타입 매개변수를 이전 타입 매개변수의 키로 제한 가능(제네릭 타입 키를 지정하는 유일한 방법)
function get<T, Key extends keyof T>(container:T,key:Key){
    //container에서 검색할 수 있는 T의 Key 중 하나의 key 이름을 받는다!?
    return container[key];
}

const roles={
    favorite:"AAA",
    others:["aaa","bbb","ccc"],
}
const favorite=get(roles,"favorite"); //타입 string
const others=get(roles,"others"); //타입 string[];

//Error:Argument of type '"extras"' is not assignable to parameter of type '"favorite" | "others"'.
const missing=get(roles,"extras");
function get<T>(container:T,key:Key){
    //container에서 검색할 수 있는 T의 Key 중 하나의 key 이름을 받는다!?
    return container[key];
}

const roles={
    favorite:"AAA",
    others:["aaa","bbb","ccc"],
}
const favorite=get(roles,"favorite"); //타입 string
const others=get(roles,"others"); //타입 string[];

//Error:Argument of type '"extras"' is not assignable to parameter of type '"favorite" | "others"'.
const missing=get(roles,"extras");
function get<T>(container:T,key:keyof T){ //union type이다 => string | string[]으로 표현된다!
    //container에서 검색할 수 있는 T의 Key 중 하나의 key 이름을 받는다!?
    return container[key];
}

const roles={
    favorite:"AAA",
    others:["aaa","bbb","ccc"],
}
const favorite=get(roles,"favorite"); 
const others=get(roles,"others"); //타입 string | string[]

//Error:Argument of type '"extras"' is not assignable to parameter of type '"favorite" | "others"'.
const missing=get(roles,"extras");
  • 위 둘의 차이의 경우: keyof T로 표현을 하면 ⇒ Container에 있는 모든 속성 값에 대한 유니언 타입이 반환된다
    • 차이점: Key extends keyof T를 사용하면 타입이 string or string[] 둘 중 하나로 고정됨
    • key: keyof T인 경우: string | string[] 타입 alias로 범위가 넓어진다.

10.7 Promise


  • 임의의 타입에 대해 유사한 작업을 나타내는 promise의 기능은 타입스크립트의 제네릭과 자연스럽게 융합된다. (Promise클래스로 표현된다???)

추가 작성이 필요하다

10.7.1 Promise 생성


  • TS에서 Promise 생성자는 단일 매개변수를 받도록 작성된다. (축소된 형식은 대략 밑과 같음)
class PromiseLike<Value>{
    constructor(
        exectutor:(
            resolve:(value:Value)=>void,
            reject:(reason:unknown)=>void,
        )=>void,
    ){}
}
  • 값을 resolve하려는 Promise를 만들려면, Promise의 타입 인수를 명시적으로 선언해야 함 ⇒ 명시하지 않으면 unknown으로 가정됨
//타입 Promise<unknown>
const resolvesUnknown=new Promise((resolve)=>{
    setTimeout(()=>resolve("A"),1000);
})

//타입: Promise<string> 
const resolveString=new Promise<string>((resolve)=>{
    setTimeout(()=>resolve("A"),1000);
})
//타입 Promise<string>
const textEventaully=new Promise<string>((resolve)=>{
    setTimeout(()=>resolve("Done!"),1000);
})

//타입: Promise<number>
const lengthEventaully=textEventaully.then((text)=>text.length);
  • .then()메서드는 반환되는 Promise의 resolve된 값을 나타내는 새로운 타입 매개변수를 받는다. (즉, text.length(number이기 때문에 ⇒ lengthEventually는 Promise<number> 타입을 가지게 된다)

10.7.2 async 함수


  • JS에서 async 키워드를 사용해 선언한 모든 함수는 Promise를 반환함
    • thenable(.then() 메서드가 있는 객체)가 아닌 경우 Promise.resolve가 호출된 것처럼 Promise로 Wrapping 된다!
//(text:string)=>Promise<string>
async function lengthAfterSecond(text:string){
    await new Promise((resolve)=>setTimeout(resolve,1000));
    return text.length;
}

//타입:(text:string)=>Promise<number>
async function lengthImmediately(text:string){
    return text.length;
}
  • .then()처럼 반환값의 타입이 Promise 제네릭 타입으로 지정됨을 확인할 수 있음
async function givesPromiseForString():Promise<string>{
    return "Done!";
}

//Error:the return type of an async function or method must be the global Promise<T> type. Did you mean to write 'Promise<string>'?
async function giveString(): string{
    return "Done";
}

10.8 제네릭 올바르게 사용하기


  • 모범사례: 필요할 때만 사용해라 남발 금지! ⇒ (범용 모듈일 경우 많이 사용하는 경우도 있음)

10.8.1 제네릭 황금률


  • 함수 타입 매개변수의 필요성을 판단하는 기준: 타입 매개변수가 최소한 두 번 이상 사용되는가?
//input 타입 매개변수를 정확히 한 번만 사용함
function logInput<Input extends string>(input:Input){
    console.log("HI",input);
}
  • 더 많은 매개변수 반환X ⇒ Input 타입 매개변수 선언은 굳이? 불필요함!!!
function logInput(input:string){
    console.log("HI",input);
}
  • 위와 같이 작성하는 것이 차라리 better (추가적인 정보는 “이펙티브 타입스크립트”에 나와 있음)

10.8.2 제네릭 명명 규칙


  • 타입 매개변수에 대한 표준 명명 규칙⇒ 첫 번째 타입 인수로 T를 사용함! ⇒ 후속은 U,V형식으로 진행한다.
  • 타입 인수의 용도에 따라 관련된 정보가 있는 경우 첫 글자를 따서 사용하기도 함(ex:상태 관리 라이브러리를 S로)
  • 하지만 단일 문자를 사용할 때 의미가 명확하지 않을 경우 타입이 사용되는 용도를 가르키는 설명적인 제네릭 타입 이름을 사용하는 것이 좋음!
function labelBox<Label,Value>(label:Label,value:Value){};