Search code examples
typescript

TS doesn't infer type has a property after conditional check


I have this TS code:

type MyFunctions = {
  play(): void
  pause(args: { reset: boolean }): void
  stop(args: { restart: boolean }): void
}

type AuthStruct = {
  [K in keyof MyFunctions]: {
    requiresAuth: boolean
  }
}

const auth = {
  play: {
    requiresAuth: true
  },
  pause: {
    requiresAuth: true
  },
  stop: {
    requiresAuth: false
  }
} as const satisfies AuthStruct

type Auth = typeof auth

type FunctionParameters<K extends keyof MyFunctions> = Parameters<
  MyFunctions[K]
>[0] extends undefined
  ? never
  : Parameters<MyFunctions[K]>[0]

type Data<K extends keyof MyFunctions> = (FunctionParameters<K> extends never
  ? {}
  : FunctionParameters<K>
) & (
    Auth[K] extends { requiresAuth: true }
    ? { token: string }
    : {}
  )

function runFunction<F extends keyof MyFunctions>(
  func: F,
  data: Data<F>
) {
  if(auth[func].requiresAuth) {
    data.token // Should work
  }
}

I would suppose that after running the conditional check if(auth[func].requiresAuth) the compiler would understand that data has a requiresAuth property. But unfortunately it doesn't work. Furthermore, I can't even force the compiler to accept that data has the token property by doing something like: data.token!. How can I solve the issue?

Playground


Solution

  • I was able to get it working with this code using typeguard and some black magic (without using Any):

    type HasToken = {token: string}
    
    const isAuth = <F extends keyof MyFunctions> (func: AuthStruct[F], _: Data<F> | HasToken): _ is HasToken => {
      return func.requiresAuth;
    }
    
    function runFunction<F extends keyof MyFunctions>(
      func: F,
      data: Data<F>
    ) {
      if(isAuth(auth[func], data)) {
        data.token // Should work
      }
    }
    

    Probably it can be improved so that you don't need to pass the func and use a bit ugly unused variable, but hey it works.

    Edit: after seeing another answer I think this might be better:

    const isAuth = <F extends keyof MyFunctions> (data: Data<F> | HasToken): data is HasToken => {
      return 'token' in data;
    }
    
    // inside of function
    if(auth[func].requiresAuth && isAuth(data)) {
        data.token // Should work
      }
    }
    

    or even like this, preserving maximum of your original logic

    const isAuth = <F extends keyof MyFunctions> (func: AuthStruct[F], data: Data<F> | HasToken): data is HasToken => {
      return func.requiresAuth && 'token' in data;
    }
    
    // inside of function
      if(isAuth(auth[func], data)) {
        data.token // Should work
      }