고급 타자 원고: 로다스를 재구성하다.얻다

금융 기구의 백엔드 팀의 일원으로서 나는 반드시 많은 복잡한 데이터 구조인 고객 개인 데이터, 거래를 처리해야 한다. 너는 마음대로 말할 수 있다.때때로 데이터 대상 내부에 깊이 들어가는 값을 제공해야 합니다.생활이 더욱 간단해지기 위해서 나는 사용할 수 있다
lodash.get 경로를 통해 값에 접근할 수 있고 끊임없는 obj.foo && obj.foo.bar 조건을 피할 수 있습니다(선택 가능한 링크가 도착한 후에 더 이상 이런 상황이 아니더라도).

이런 방법은 무슨 문제가 있습니까?

_.get는 실행할 때 잘 작동하지만 TypeScript와 함께 사용할 때 큰 단점이 있다. 대부분의 경우 값 유형을 추정할 수 없기 때문에 재구성 과정에서 여러 가지 문제가 발생할 수 있다.
예를 들어 서버가 우리에게 데이터를 보내고 이런 방식으로 고객의 주소를 저장한다
type Address = {
  postCode: string
  street: [string, string | undefined]
}

type UserInfo = {
  address: Address
  previousAddress?: Address
}

const data: UserInfo = {
  address: {
    postCode: "SW1P 3PA",
    street: ["20 Deans Yd", undefined]
  }
}
지금 저희가 렌더링을 해야 돼요.
import { get } from 'lodash'

type Props = {
  user: UserInfo
}
export const Address = ({ user }: Props) => (
  <div>{get(user, 'address.street').filter(Boolean).join(', ')}</div>
)
잠시 후, 우리는 이 데이터 구조를 재구성하고, 약간의 다른 주소 표시를 사용할 것이다
type Address = {
  postCode: string
  street: {
    line1: string
    line2?: string
  }
}
_.get가 항상 경로 문자열로 되돌아오기 때문에any TypeScript는 아무런 문제도 알아채지 못하고 코드는 실행할 때 던져집니다. filter 방법은 우리의 새 Address 대상에 존재하지 않기 때문입니다.

유형 추가


v4부터 시작합니다.TypeScript는 2020년 11월에 출시되었는데 Template Literal Types라는 기능을 가지고 있다.이것은 우리가 문자와 다른 형식으로 템플릿을 구축할 수 있도록 합니다.그것이 우리를 도울 수 있는 것이 무엇인지 보여 주시오.

해석점 구분 경로


가장 흔히 볼 수 있는 장면에 대해 TypeScript는 대상 내의 주어진 경로를 통해 값 유형을 정확하게 추정하기를 바랍니다.위의 예시에서 우리는 업데이트된 데이터 구조를 통해 문제를 미리 주의할 수 있도록 address.street의 유형을 알고 싶다.나는 또 사용할 것이다Conditional Types.만약 조건 유형에 익숙하지 않다면, 그것을 간단한 삼원 연산자로 상상해 보세요. 이것은 한 유형이 다른 유형과 일치하는지 알려 줍니다.
우선, 우리의 경로가 하나의 점으로 구분된 필드인지 확인해 봅시다
type IsDotSeparated<T extends string> = T extends `${string}.${string}`
  ? true
  : false

type A = IsDotSeparated<'address.street'> // true
type B = IsDotSeparated<'address'> // false
간단해 보이죠?그런데 우리는 어떻게 해야만 진정한 키를 추출할 수 있습니까?
여기에 신기한 키워드 infer 가 있습니다. 문자열의 일부분을 얻을 수 있도록 도와줍니다.
type GetLeft<T extends string> = T extends `${infer Left}.${string}`
  ? Left
  : undefined

type A = GetLeft<'address.street'> // 'address'
type B = GetLeft<'address'> // undefined
이제 우리의 대상 유형을 추가할 때가 되었다.간단한 사례부터 시작해 보도록 하겠습니다.
type GetFieldType<Obj, Path> = Path extends `${infer Left}.${string}`
  ? Left extends keyof Obj
    ? Obj[Left]
    : undefined
  : Path extends keyof Obj
    ? Obj[Path]
    : undefined


type A = GetFieldType<UserInfo, 'address.street'> // Address, for now we only taking a left part of a path
type B = GetFieldType<UserInfo, 'address'> // Address
type C = GetFieldType<UserInfo, 'street'> // undefined
우선, 전달된 경로가 string.string 템플릿과 일치하는지 확인합니다.만약 그렇다면, 우리는 그 왼쪽 부분을 취하여, 대상의 키에 존재하는지 확인하고, 필드 형식으로 되돌아갈 것입니다.
경로가 템플릿과 일치하지 않으면 간단한 키가 될 수 있습니다.이 경우, 우리는 유사한 검사를 진행하고, 필드 형식 undefined 을 백업으로 되돌려줍니다.

반복 추가


알겠습니다. 최고급 필드의 정확한 유형을 찾았습니다.하지만 그것은 우리에게 약간의 가치를 주었다.실용 프로그램 형식을 개선하고 필요한 값의 경로를 따라 전진합시다.
Dell은 다음을 수행할 것입니다.
  • 최상위 레벨 키 찾기
  • 주어진 키
  • 로 값 얻기
  • 경로에서 이 키 제거
  • 해석 값과 키의 나머지 부분에 대해 Left.Right가 일치하지 않을 때까지 전체 과정을 반복한다.
  • export type GetFieldType<Obj, Path> =
      Path extends `${infer Left}.${infer Right}`
        ? Left extends keyof Obj
          ? GetFieldType<Obj[Left], Right>
          : undefined
        : Path extends keyof Obj
          ? Obj[Path]
          : undefined
    
    type A = GetFieldType<UserInfo, 'address.street'> // { line1: string; line2?: string | undefined; }
    type B = GetFieldType<UserInfo, 'address'> // Address
    type C = GetFieldType<UserInfo, 'street'> // undefined
    
    완벽해!보아하니 이것이 바로 우리가 원하는 것 같다.

    옵션 속성 작업


    우리는 아직 사건 하나를 고려해야 한다.UserInfo 유형에는 선택 사항previousAddress 필드가 있습니다.previousAddress.street 유형을 입력해 보겠습니다.
    type A = GetFieldType<UserInfo, 'previousAddress.street'> // undefined
    
    아이고!그러나 previousAddress가 설정되어 있으면 street는 정의되지 않은 것이 아닐 것이다.
    여기서 무슨 일이 일어났는지 봅시다.previousAddress는 선택할 수 있기 때문에 그 종류는 Address | undefined입니다. (저는 당신이 이미 열었다고 가정합니다strictNullChecks.분명히 streetundefined에는 존재하지 않기 때문에 정확한 유형을 추정할 수 없다.
    우리는 우리의 것을 개선해야 한다 GetField.정확한 유형을 검색하려면 삭제해야 합니다 undefined.그러나, 이 필드는 선택할 수 있고, 이 값은 사실상 정의되지 않았을 수도 있기 때문에, 최종 형식에 그것을 보존해야 한다.
    다음과 같은 두 가지 기본 TypeScript 유틸리티 유형을 사용할 수 있습니다.Exclude 주어진 연합에서 유형을 삭제하고 Extract 주어진 연합에서 유형을 추출하거나 일치하는 항목이 없는 상황에서 되돌아오기never.
    export type GetFieldType<Obj, Path> = Path extends `${infer Left}.${infer Right}`
      ? Left extends keyof Obj
        ? GetFieldType<Exclude<Obj[Left], undefined>, Right> | Extract<Obj[Left], undefined>
        : undefined
      : Path extends keyof Obj
        ? Obj[Path]
        : undefined
    
    // { line1: string; line2?: string | undefined; } | undefined
    type A = GetFieldType<UserInfo, 'previousAddress.street'>
    
    undefined가 값 유형에 나타나면 | Extract<> 결과에 추가됩니다.그렇지 않으면 Extract 되돌아오기 never, 이 값은 무시됩니다.
    그렇지!현재 우리는 아주 좋은 실용 프로그램 형식이 생겼는데, 이것은 우리의 코드를 더욱 안전하게 하는 데 도움이 될 것이다.

    효용 함수 실현


    TypeScript에서 정확한 값 유형을 얻는 방법을 가르쳤으니, 실행 시 논리를 추가합시다.우리는 함수가 한 점으로 구분된 경로를 여러 부분으로 나누고 이 목록을 줄여서 최종 값을 얻기를 희망합니다.함수 자체는 매우 간단하다.
    export function getValue<
      TData,
      TPath extends string,
      TDefault = GetFieldType<TData, TPath>
    >(
      data: TData,
      path: TPath,
      defaultValue?: TDefault
    ): GetFieldType<TData, TPath> | TDefault {
      const value = path
        .split('.')
        .reduce<GetFieldType<TData, TPath>>(
          (value, key) => (value as any)?.[key],
          data as any
        );
    
      return value !== undefined ? value : (defaultValue as TDefault);
    }
    
    우리는 추한 as any형 주물을 첨가해야 한다. 왜냐하면
  • 중간값은 사실상 모든 유형이 될 수 있다.
  • Array.reduce 초기 값의 유형이 같기를 기대합니다.그러나 이곳의 상황은 그렇지 않다.
    그 밖에 세 개의 범용 유형 매개 변수가 있지만, 우리는 그곳에서 어떤 유형도 제공할 필요가 없다.모든 범주가 함수 매개변수에 매핑되므로 TypeScript는 함수를 호출할 때 실제 값을 기준으로 이러한 매개변수를 추정합니다.
  • 구성 요소 유형 보안


    우리 조립품을 다시 한 번 복습합시다.최초의 실현에서, 우리는 lodash.get 을 사용했는데, 일치하지 않는 유형에 대한 오류가 발생하지 않았다.그러나 우리의 새로운 모델getValue이 생기면 TypeScript는 즉각 불평을 시작할 것이다

    [] 기호에 대한 지원 추가

    _.get 지원list[0].foo 등키.우리 유형에서 같은 기능을 실현합시다.마찬가지로 텍스트 템플릿 형식은 네모난 괄호에서 색인 키를 가져오는 데 도움을 줄 것입니다.이번에 나는 한 걸음 한 걸음 말하지 않고, 아래에서 최종 유형과 평론을 발표할 것이다.
    type GetIndexedField<T, K> = K extends keyof T 
      ? T[K]
      : K extends `${number}`
        ? '0' extends keyof T
          ? undefined
          : number extends keyof T
            ? T[number]
            : undefined
        : undefined
    
    type FieldWithPossiblyUndefined<T, Key> =
      | GetFieldType<Exclude<T, undefined>, Key>
      | Extract<T, undefined>
    
    type IndexedFieldWithPossiblyUndefined<T, Key> =
      | GetIndexedField<Exclude<T, undefined>, Key>
      | Extract<T, undefined>
    
    export type GetFieldType<T, P> = P extends `${infer Left}.${infer Right}`
      ? Left extends keyof T
        ? FieldWithPossiblyUndefined<T[Left], Right>
        : Left extends `${infer FieldKey}[${infer IndexKey}]`
          ? FieldKey extends keyof T
            ? FieldWithPossiblyUndefined<IndexedFieldWithPossiblyUndefined<T[FieldKey], IndexKey>, Right>
            : undefined
          : undefined
      : P extends keyof T
        ? T[P]
        : P extends `${infer FieldKey}[${infer IndexKey}]`
          ? FieldKey extends keyof T
            ? IndexedFieldWithPossiblyUndefined<T[FieldKey], IndexKey>
            : undefined
          : undefined
    
    모듈이나 그룹에서 값을 검색하려면 새 GetIndexedField 유틸리티 형식이 있습니다.이것은 주어진 키를 통해 모듈 값을 되돌려줍니다. 키가 모듈 범위를 초과하면 정의되지 않은 값을 되돌려주고, 일반적인 그룹에 대해서는 요소 형식을 되돌려줍니다.'0' extends keyof T 조건은 문자열 키가 없기 때문에 값이 원조인지 확인합니다.만약 당신이 원조와 수조를 구분하는 더 좋은 방법을 알고 있다면 저에게 알려주세요.
    우리는 ${infer FieldKey}[${infer IndexKey}] 템플릿을 사용하여 field[0] 부분을 해석한다.그리고 이전과 같은 Exclude | Extract 기술을 사용하여 우리는 선택 가능한 속성과 관련된 값 유형을 검색합니다.
    지금 우리는 getValue 함수를 약간 수정해야 한다.간단하게 보기 위해서, 나는 .split('.').split(/[.[\]]/).filter(Boolean)로 바꾸어 새로운 기호를 지원할 것이다.이것은 이상적인 해결 방안이 아닐 수도 있지만, 더욱 복잡한 해석은 본문의 범위를 넘어섰다.
    다음은 최종적인 실현이다
    export function getValue<
      TData,
      TPath extends string,
      TDefault = GetFieldType<TData, TPath>
    >(
      data: TData,
      path: TPath,
      defaultValue?: TDefault
    ): GetFieldType<TData, TPath> | TDefault {
      const value = path
        .split(/[.[\]]/)
        .filter(Boolean)
        .reduce<GetFieldType<TData, TPath>>(
          (value, key) => (value as any)?.[key],
          data as any
        );
    
      return value !== undefined ? value : (defaultValue as TDefault);
    }
    

    결론


    현재 우리는 좋은 실용 함수를 가지고 있을 뿐만 아니라 코드 유형의 안전성을 높일 수 있을 뿐만 아니라 어떻게 실천에서 템플릿 텍스트와 조건 유형을 응용하는지 더욱 잘 이해할 수 있다.
    나는 이 문장이 도움이 되기를 바란다.읽어주셔서 감사합니다.
    모든 코드는 this codesandbox를 통해 얻을 수 있습니다.

    좋은 웹페이지 즐겨찾기