I've got an AuthStatus
enum like:
enum AuthStatus {
UNAUTHENTICATED,
ONBOARDING,
AUTHENTICATED
}
Now, I would like to declare a type as following:
type AuthType<T extends AuthStatus> = {
accepted: T,
navigateTo: Record<T, string>
}
where the navigateTo
property should be a Record
with the keys of the enum values, but excluding the one we choose as accepted
.
For example:
/* 'exampleA' accepts 'UNAUTHENTICATED' users.
'UNAUTHENTICATED' should be excluded from the 'navigateTo' object.
*/
const exampleA: AuthType<AuthStatus> = {
accepted: AuthStatus.UNAUTHENTICATED,
navigateTo: {
[AuthStatus.ONBOARDING]: '/onboarding',
[AuthStatus.AUTHENTICATED]: '/',
}
}
/* 'exampleB' accepts 'ONBOARDING' users.
'ONBOARDING' should be excluded from the 'navigateTo' object.
*/
const exampleB: AuthType<AuthStatus> = {
accepted: AuthStatus.ONBOARDING,
navigateTo: {
[AuthStatus.UNAUTHENTICATED]: '/login',
[AuthStatus.AUTHENTICATED]: '/',
}
}
/* 'exampleC' accepts 'AUTHENTICATED' users.
'AUTHENTICATED' should be excluded from the 'navigateTo' object.
*/
const exampleC: AuthType<AuthStatus> = {
accepted: AuthStatus.AUTHENTICATED,
navigateTo: {
[AuthStatus.UNAUTHENTICATED]: '/login',
[AuthStatus.ONBOARDING]: '/onboarding',
}
}
How to declare the AuthType to make sure the keys of the record excludes the accepted value?
I'd say you want AuthType<T>
to be a union where for each member of the union T
, there's a member of the union AuthType<T>
where that member is accepted
and where navigateTo
has all keys in T
except that member. You can write it as a distributive object type (as coined in microsoft/TypeScript#47109), a mapped type into which you index:
type AuthType<T extends PropertyKey> = { [K in T]: {
accepted: K,
navigateTo: Record<Exclude<T, K>, string>
} }[T]
The important bit there is the Exclude
utility type which filters union members. For AuthStatus
that becomes
type AuthTypeForAuthStatus = AuthType<AuthStatus>
/* type AuthTypeForAuthStatus = {
accepted: AuthStatus.UNAUTHENTICATED;
navigateTo: Record<AuthStatus.ONBOARDING | AuthStatus.AUTHENTICATED, string>;
} | {
accepted: AuthStatus.ONBOARDING;
navigateTo: Record<AuthStatus.UNAUTHENTICATED | AuthStatus.AUTHENTICATED, string>;
} | {
accepted: AuthStatus.AUTHENTICATED;
navigateTo: Record<AuthStatus.UNAUTHENTICATED | AuthStatus.ONBOARDING, string>;
} */
as desired. Or for some other set of keys:
type Test = AuthType<"a" | "b" | "c" | "d">;
/* type Test = {
accepted: "a";
navigateTo: Record<"b" | "c" | "d", string>;
} | {
accepted: "b";
navigateTo: Record<"a" | "c" | "d", string>;
} | {
accepted: "c";
navigateTo: Record<"a" | "b" | "d", string>;
} | {
accepted: "d";
navigateTo: Record<"a" | "b" | "c", string>;
} */
Your examples then work as written:
const exampleA: AuthType<AuthStatus> = {
accepted: AuthStatus.UNAUTHENTICATED,
navigateTo: {
[AuthStatus.ONBOARDING]: '/onboarding',
[AuthStatus.AUTHENTICATED]: '/',
}
} // okay, etc