고급 타자 원고: 로다스를 재구성하다.얻다
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
.분명히 street
undefined
에는 존재하지 않기 때문에 정확한 유형을 추정할 수 없다.우리는 우리의 것을 개선해야 한다
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.)