함수의 리턴값이 배열일 때 타입을 명시적으로 지정하지 않고 추론을 이용하는 방법은?

타입스크립트는 타입 추론 덕분에 함수 선언시 별다른 것을 하지 않아도 자동완성이 잘 될 때 참 편하네요. 아래와 같이 object 형태로 key value 로 지정한 경우에는 추론이 잘되는데 리턴 타입을 배열로 했을 경우 추론이 잘 되지 않아서 불편합니다. 명시적으로 지정하지 않고 간단히 하는 방법은 없는지 궁금해서 질문남깁니다.

object 리턴

function someFunction(){
  const state = {
    name: ''
  };
  const actions = {
    login: async()=>{},
    logout: async()=>{}
  }
  return [state, actions];
}

const [state, actions] = someFunction();

// 여기에서 state.n 을 누르면 추론에 의해 name이 자동완성 됩니다.

배열 리턴

function someFunction(){
  const state = {
    name: ''
  };
  const actions = {
    login: async()=>{},
    logout: async()=>{}
  }
  return [state, actions];
}

const [state, actions] = someFunction();

// 여기에서 state.n 을 누르면 추론에 의해 name이 자동완성 되지 않습니다.

이를 해결하기 위해 다음과 같이 명시적으로 타입을 지정해주면 잘 되는데 매번 지정하기 귀찮네요. 간단히 하는 방법은 없을까요?

interface State {
  name: string
};

interface Actions {
  login: ()=> Promise<any>
  logout: ()=> Promise<any>
}
function someFunction():[State,Actions]{
  const state = {
    name: ''
  };
  const actions = {
    login: async()=>{},
    logout: async()=>{}
  }
  return [state, actions];
}

const [state, actions] = someFunction();

여기까지 읽어주셔서 감사합니다. :slight_smile:

현재 버전의 타입스크립트의 타입 추론 매커니즘을 따르면,
리턴된 배열의 원소값에 대해서 추론까지 하지만
그것이 인덱스마다 어떻게 매핑되어 있는지는 고려하지 않는 것 같습니다.

이는 타입스크립트에서 바라보는 배열의 기능성에 대한 관점이
List와 Tuple 중, List에 중점을 두고 있기 때문이 아닌가 합니다.

속성 suggest를 하지 않음에도
추론까지는 한다고 말씀드린 이유는 다음과 같은데요.

function someFunction() {
    const state = {
        name: ''
    };
    const actions = {
        login: async () => { },
        logout: async () => { }
    }
    return [state, actions];
}

const [state, actions] = someFunction()

위 코드에서 마우스 커서를 state에 올려보면
image
다음과 같은 팝업을 볼 수 있습니다.

// 어쨌든
// const state로 선언한 타입과
{ name: string; }

// const actions로 선언한 타입을 찾았다는 거죠.
{ login: () => Promise<void>; logout: () => Promise<void> }

이는 추론의 과정이 다음과 같아서 일 겁니다.

function someFunction() {
    // 타입스크립트의 추론 과정의 이해를 돕기 위해
    // 함수가 배열을 리턴한다고 가정하면,
    const returnArr = [];
    // 이 시점에서는 리턴 타입은 any[]

    const state = { name: ''};
    returnArr.push(state);
    // 이 시점에서는 리턴 타입은  { name: string; }[]

    const actions = {
        login: async () => { },
        logout: async () => { }
    }
    returnArr.push(actions);
    // 이 시점에서는 리턴 타입은  ({ name: string; } | { login: () => Promise<void>; logout: () => Promise<void> })[]

    return returnArr;
}

과정은 살짝 다르지만 결론적으로 someFunction 함수의 리턴 타입은

({ name: string; } | { login: () => Promise<void>; logout: () => Promise<void> })[]

이렇게 됩니다.

이걸 구조 분해로 받아도 저 위의 두 타입 중 하나인 걸로만 추론되었기 때문에
타입 기반 suggest를 해 줄 수 없는 거죠. 둘 중 뭔지 모르니까요.

인덱스 기반으로 타입을 특정하여 추론까지 해 주지 않습니다.
그냥 둘 중 하나라는 거죠.(Best common type)

반례로 만약 const actions에도 name: string 속성값이 있으면 name 을 suggest 해 줄 겁니다.

혹은 type assertion을 통해 타입을 특정해 주면 에러도 없으며 원하는대로 동작할 겁니다.

(state as { name: string }).name

타입스크립트는 그 컨셉상
배열을 바라보는 관점이
여러타입을 넣을 수 있는 Tuple로써가 아니라
같은 타입을 넣는 컬렉션의 의미가 강한 List로 바라보는 것 같습니다.
먄약 Tuple의 성격이 강했다면 위와 같이 추론하지 않았을거에요.

그래서 결국 질문의 경우에는
배열로 하는 간단한 방법은 없을 것 같습니다.
번거로움을 덜기 위한 목적이라면 객체로 리턴하는게 맞는 것 같아요.

참고: https://2ality.com/2020/02/typing-arrays-typescript.html

1개의 좋아요

아주 자세한 설명과 관련 링크까지 달아주신 점 정말 감사합니다. :pray:
덕분에 새로운 사실들을 많이 알게 되었어요.

설명해주신 내용중에 인상적인 부분을 요약해보자면

Best common type

정말 actions 에도 name 속성이 있다면 suggest 해주네요. 새롭게 알았습니다. 제가 사용하는 VSCode 환경에서는 JavaScript는 actions에 없어도 전부 추천해줍니다. JS에서 합집합으로, TS에는 교집합으로 추천해주는 샘이네요. (VSCode에서 어떤 Exstention이 정확히 이 속성의 합집합으로 suggest해주는지도 궁금해지네요.)
아래 사진은 JS에서 위 코드를 넣었을 때 name, login, logout 모두 추천해주는 모습니다.
image

as const

@digda 님이 남겨주신 링크에서 제가 원하는 답을 찾았습니다.!!
as const를 하면 정상 작동 합니다. 감사합니다. :pray:

image

2개의 좋아요

return […] as const; 로 하시면 리턴타입이 튜플로 잡힙니다.

최근에 TypeScript Korea 에서 튜플타입을 다루는 방법을 잠깐 소개한 적이 있는데요 (https://youtu.be/bfSKqscC8kc?t=2705)

응용하면 요렇게 튜플 타입을 매핑하거나 할 수도 있습니다.

3개의 좋아요

@cometkim 영상 링크공유 감사합니다. 영상에서 이런 저런 재밌는 걸 많이 보여주시는데 제가 아직 익숙하지 않아서 한번에 이해되지 않네요.ㅎㅎ 다시 한번 살펴보면서 관련 내용을 찾아봐야겠어요. ㅎ

1개의 좋아요

링크에 답이 있었군요. 끝까지는 안 읽어봐서 ㅋㅋ
저도 배우고 갑니다.