GraphQL 유형 보호 장치

GraphQL을 사용할 때 응답의 유형을 단언해야 할 때가 있습니다.때로는 응답이 연합 형식이기 때문이고, 때로는 응답이 비어 있기 때문이다.이것은 보통 개발자로 하여금 응답 유형을 빈번하게 단언하게 하기 때문에 약간의 소음을 일으킬 수 있다.
이러한 단언을 처리하기 위해서, 우리는 몇 가지 유용한 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 패키지에 포장했다.

좋은 웹페이지 즐겨찾기