고급 타자 원고: 로다스를 재구성하다.얻다
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를 통해 얻을 수 있습니다.
Reference
이 문제에 관하여(고급 타자 원고: 로다스를 재구성하다.얻다), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://dev.to/tipsy_dev/advanced-typescript-reinventing-lodash-get-4fhe텍스트를 자유롭게 공유하거나 복사할 수 있습니다.하지만 이 문서의 URL은 참조 URL로 남겨 두십시오.
우수한 개발자 콘텐츠 발견에 전념
(Collection and Share based on the CC Protocol.)