GraphQL 유형 보호 장치
53995 단어 reacttypescriptgraphqlnode
이러한 단언을 처리하기 위해서, 우리는 몇 가지 유용한 typeguards 함수를 볼 것이다. 그것이 바로 isType, isEither, isNot, isTypeInTuple이다.
간단한 용례
예를 들어 다음과 같은 돌연변이 응답 결과를 단언할 때 개발자는ActiveUser, UserAuthenticationError, InvalidArgumentError 등 세 가지 다른 상황을 처리해야 한다.
mutation CreateUser($name: String!, $email: String!) {
createUser(name: $name, email: $email) {
... on ActiveUser {
id
name
status
email
}
... on UserAuthenticationError {
code
message
}
... on InvalidArgumentsError {
code
message
invalidArguments {
key
message
}
}
}
}
그것은 이렇게 보일 수도 있다.const initialUserState = {
name: '',
email: ''
}
function UserForm() {
const [{ name, email }, setState] = useState(initialUserState)
const [errors, setErrors] = useState({})
const [saveUser] = useCreateUserMutation({
variables: {
name,
email
}
})
async function handleSubmit(event) {
event.preventDefault()
const { data } = await saveUser()
switch (data.createUser.__typename) {
case 'ActiveUser':
setState(initialUserState)
setErrors({})
case 'UserAuthenticationError':
// Display missing authentication alert / toast
case 'InvalidArgumentsError':
setErrors(toErrorRecord(data.createUser.invalidArguments))
default:
break
}
}
return (
//... Form JSX
)
}
이 간단한 용례로 말하자면, 이것은 매우 좋다.그러나 만약에 우리가 클라이언트 apollo 클라이언트 캐시를 업데이트하고 새로 만든 사용자를 캐시에 포함시키려면 어떻게 해야 합니까?그런 다음 handleSubmit 함수는 다음과 같습니다.
async function handleSubmit(event) {
event.preventDefault()
const { data } = await saveUser({
update: (cache, { data: { createUser } }) => {
const existingUsers = cache.readQuery<UsersQuery>({ query: GET_USERS })
if (data.createUser.__typename === 'ActiveUser') {
cache.writeQuery({
query: GET_USERS,
data: {
users: [...existingUsers.users, createUser]
}
})
}
}
})
switch (data.createUser.__typename) {
case 'ActiveUser':
setState(initialUserState)
setErrors({})
case 'UserAuthenticationError':
// Display missing authentication alert / toast
case 'InvalidArgumentsError':
setErrors(toErrorRecord(data.createUser.invalidArguments))
default:
break
}
}
이것도 좋지만, 우리는 이미 여러 개의 를 가지고 있다.유형명 단언.이것은 곧 통제력을 잃을 것이다.이때 실용적인 유형의 보호 기능을 사용할 수 있다.typename 속성을 기반으로 간단한 isType typeguard를 생성합니다.
isType
type GraphQLResult = { __typename: string }
type ValueOfTypename<T extends GraphQLResult> = T['__typename']
function isType<Result extends GraphQLResult, Typename extends ValueOfTypename<Result>>(
result: Result,
typename: Typename
): result is Extract<Result, { __typename: Typename }> {
return result?.__typename === typename
}
이 typeguard를 사용하면 is
표현식이 있는 Typescript Extract 실용 프로그램 형식을 사용하여 Typescript 컴파일러에게 우리의 결과가 어떤 종류인지 알려 줍니다.이제 제출 기능은 다음과 같습니다.
async function handleSubmit(event) {
event.preventDefault()
const { data } = await saveUser({
update: (cache, { data: { createUser } }) => {
const existingUsers = cache.readQuery<UsersQuery>({ query: GET_USERS })
if (isType(createUser, 'ActiveUser')) {
cache.writeQuery({
query: GET_USERS,
data: {
users: [...existingUsers.users, createUser]
}
})
}
}
})
if (isType(data?.createUser, 'ActiveUser')) {
setState(initialUserState)
setErrors({})
} else if (isType(data?.createUser, 'UserAuthenticationError')) {
// Display missing authentication alert / toast
} else if (isType(data?.createUser, 'InvalidArgumentsError')) {
setErrors(toErrorRecord(data.createUser.invalidArguments))
}
}
더 좋은 것은 우리가 일부 유형의 안전성을 얻었다는 것이다. isType의 typename 매개 변수는 자동적으로 완성되는 기능이 있고 논리는 쉽게 읽고 명확하게 할 수 있다.물론 이것은 주요한 개선이 아니지만 isType 함수는 여러 가지 다른 방식으로 더욱 복잡한 상황을 처리할 수 있다.
더 복잡한 용례
이제 GET 사용자가 다음과 같이 질의한다고 가정합니다.
query Users {
users {
... on ActiveUser {
id
name
status
email
posts {
id
title
}
}
... on DeletedUser {
id
name
status
deletedAt
}
... on BannedUser {
id
name
status
banReason
}
}
}
GraphQL 반환 유형은 다음과 같습니다.union UserResult =
ActiveUser
| BannedUser
| DeletedUser
| InvalidArgumentsError
| UserAuthenticationError
우리는 사용자의 상태를 바꾸고 그에 상응하는 캐시를 업데이트해서 사용자의 업데이트 상태를 반영하기를 희망합니다.우리는 이런 돌변이 있을 것이다.
mutation ChangeUserStatus($status: UserStatus!, $id: Int!) {
changeUserStatus(status: $status, id: $id) {
... on ActiveUser {
id
name
status
email
posts {
id
title
}
}
... on DeletedUser {
id
name
status
deletedAt
}
... on BannedUser {
id
name
status
banReason
}
... on UserAuthenticationError {
code
message
}
... on InvalidArgumentsError {
code
message
invalidArguments {
key
message
}
}
}
}
이제 이러한 변이를 실현하고 응답 유형에 따라 캐시를 업데이트하기 위해 다음과 같은 내용이 있습니다.const [changeUserStatus] = useChangeUserStatusMutation({
update: (cache, { data: { changeUserStatus } }) => {
const existingUsers = cache.readQuery<UsersQuery>({ query: GET_USERS })
const filteredUsers = existingUsers.users.filter(
(user) =>
(user.__typename === 'ActiveUser' ||
user.__typename === 'DeletedUser' ||
user.__typename === 'BannedUser') &&
(changeUserStatus.__typename === 'ActiveUser' ||
changeUserStatus.__typename === 'DeletedUser' ||
changeUserStatus.__typename === 'BannedUser') &&
user.id !== changeUserStatus.id
)
cache.writeQuery({
query: GET_USERS,
data: {
users: [...filteredUsers, changeUserStatus]
}
})
}
})
이것은 좀 지루하다.isType 함수를 사용하여 소음을 조금 줄일 수 있습니다.const [changeUserStatus] = useChangeUserStatusMutation({
update: (cache, { data: { changeUserStatus } }) => {
const existingUsers = cache.readQuery<UsersQuery>({ query: GET_USERS })
const filteredUsers = existingUsers.users.filter(
(user) =>
(isType(user, 'ActiveUser') ||
isType(user, 'DeletedUser') ||
isType(user, 'BannedUser')) &&
(isType(changeUserStatus, 'ActiveUser') ||
isType(changeUserStatus, 'DeletedUser') ||
isType(changeUserStatus, 'BannedUser')) &&
user.id !== changeUserStatus.id
)
cache.writeQuery({
query: GET_USERS,
data: {
users: [...filteredUsers, changeUserStatus]
}
})
}
})
그러나 이것은 여전히 그다지 좋지 않다.사용자와 돌연변이 결과가ActiveUser, DeletedUser, BannedUser인지 확인하는 데 도움을 줄 typeguard를 구축해야 할지도 모른다.또는 사용자와 돌연변이 결과가 User Authentication Error나 Invalid Arguments Error가 아니라는 것을 단언하기 위한 배제 함수가 있어야 합니다.
isEither 함수부터 시작하겠습니다.
이세세
type GraphQLResult = { __typename: string }
type ValueOfTypename<T extends GraphQLResult> = T['__typename']
function isEither<
Result extends GraphQLResult,
Typename extends ValueOfTypename<Result>,
PossibleTypes extends Array<Typename>
>(
result: Result,
typenames: PossibleTypes
): result is Extract<Result, { __typename: typeof typenames[number] }> {
const types = typenames?.filter((type) => isType(result, type))
return types ? types.length > 0 : false
}
이 isEither 함수는 지정된 유형 이름을 반복할 때만 isType 함수를 조합합니다.유형 선언은 다음과 같습니다.
result is Extract<Result, { __typename: typeof typenames[number] }>
그것은 결과가 typenames 그룹의 인덱스 값의 합계 중 하나라고 단언했다.현재 우리 changeUserStatus 돌연변이와 캐시 업데이트는 이렇게 재구성할 수 있습니다.
const [changeUserStatus] = useChangeUserStatusMutation({
update: (cache, { data: { changeUserStatus } }) => {
const existingUsers = cache.readQuery<UsersQuery>({ query: GET_USERS })
const filteredUsers = existingUsers.users.filter(
(user) =>
isEither(user, ['ActiveUser', 'BannedUser', 'DeletedUser']) &&
isEither(changeUserStatus, ['ActiveUser', 'BannedUser', 'DeletedUser']) &&
user.id !== changeUserStatus.id
)
cache.writeQuery({
query: GET_USERS,
data: {
users: [...filteredUsers, changeUserStatus]
}
})
}
})
많이 좋아졌어!이제 isNot 함수를 살펴보겠습니다.안 그래?
type GraphQLResult = { __typename: string }
type ValueOfTypename<T extends GraphQLResult> = T['__typename']
function isNot<
Result extends GraphQLResult,
Typename extends ValueOfTypename<Result>,
ExcludedTypes extends Array<Typename>
>(
result: Result,
typenames: ExcludedTypes
): result is Exclude<Result, { __typename: typeof typenames[number] }> {
const types = typenames?.filter((type) => isType(result, type))
return types ? types.length === 0 : false
}
보시다시피 isNot 함수는 사실상 isEither 함수의 거울입니다.추출된 프로그램 형식이 아닌 Exclude 형식을 사용합니다. 실행할 때 검증하면 반대로 형식 길이가 0인지 확인합니다.
const [changeUserStatus] = useChangeUserStatusMutation({
update: (cache, { data: { changeUserStatus } }) => {
const existingUsers = cache.readQuery<UsersQuery>({ query: GET_USERS })
const filteredUsers = existingUsers.users.filter(
(user) =>
isNot(user, ['UserAuthenticationError', 'InvalidArgumentsError']) &&
isNot(changeUserStatus, ['UserAuthenticationError', 'InvalidArgumentsError']) &&
user.id !== changeUserStatus.id
)
cache.writeQuery({
query: GET_USERS,
data: {
users: [...filteredUsers, changeUserStatus]
}
})
}
})
마지막으로 isTypeInTuple 함수를 살펴보겠습니다. 이것은 원조에서 유형을 필터하는 데 도움을 줄 것입니다.IstypeIntouple
지금 우리는 같은 검색어를 가지고 있지만, 우리의ActiveUsers, DeletedUsers,BannedUsers를 다른 목록에 보여주기를 희망한다.
이를 위해서는 세 개의 서로 다른 어레이로 사용자를 필터링해야 합니다.
const { data, loading } = useUsersQuery()
const activeUsers = useMemo(
() => data?.users?.filter((user) => isType(user, 'ActiveUser')) ?? [],
[data]
)
사람들은 이전의 필터가 실행할 때 정확한 사용자를 얻을 수 있을 뿐만 아니라, 확실히 그렇다고 생각할 수도 있다.그러나 유감스럽게도 Typescript는 현재의 activeUsers가 하나의 그룹 activeUsers일 뿐이라는 것을 알지 못한다.따라서 activeUsers 배열을 사용할 때, 우리는 짜증나고 근거가 없는 유형의 오류를 만날 수 있습니다.이 문제를 처리하기 위해서, 우리는activeUsers 그룹을 강제로
Array<ActiveUser>
변환해야 할 수도 있지만, 만약 우리가 유형의 강제 변환을 피할 수 있다면, 왜 이렇게 하지 않겠습니까?이것이 바로 ISTYPEINTULE의 역할이다.type GraphQLResult = { __typename: string }
type ValueOfTypename<T extends GraphQLResult> = T['__typename']
export function isTypeInTuple<
ResultItem extends GraphQLResult,
Typename extends ValueOfTypename<ResultItem>
>(
typename: Typename
): (resultItem: ResultItem) => resultItem is Extract<ResultItem, { __typename: Typename }> {
return function (
resultItem: ResultItem
): resultItem is Extract<ResultItem, { __typename: Typename }> {
return isType(resultItem, typename)
}
}
리셋 함수를 되돌려줌으로써typescript 호출이 지정한 형식으로 되돌아오는 것을 알 수 있습니다.단언 형식의 방식은 우리의 다른 함수와 유사하다.그러나 우리는 typeguard 반환 유형을 단언하는 것이 아니라 반환 자체의 유형을 단언한다.
(resultItem: ResultItem) => resultItem is Extract<ResultItem, { __typename: Typename }>
이것은 typescript에서 무엇을 얻을 수 있는지 알려 줍니다.이제 다음과 같이 사용할 수 있습니다.const activeUsers = useMemo(() => data?.users?.filter(isTypeInTuple('ActiveUser')) ?? [], [data])
올바른 유형의 ActiveUser 배열이 제공됩니다.만약 이것이 매우 유용하다고 생각하고 이 함수들을 사용하고 싶다면, 나는 그것들을 gql-typeguards 라는 npm 패키지에 포장했다.
Reference
이 문제에 관하여(GraphQL 유형 보호 장치), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://dev.to/nicolastoulemont/graphql-typeguards-1854텍스트를 자유롭게 공유하거나 복사할 수 있습니다.하지만 이 문서의 URL은 참조 URL로 남겨 두십시오.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)