typescript

How to enforce an argument in TypeScript to be the result of a specific function?


const x = () => {
  return 1 + 2
}

const y = () => {
  return 1 + 2
}

type TArg = ?

const fn = (arg: TArg) => {
  console.log(arg)
}

// I want 'fn' to only accept the result of 'x' and throw an error if 'y()' is passed
fn(y()) // This should throw a type error
fn(x()) // Only this should be valid

I want to ensure that the function fn only accepts the return value of the function x and throws a TypeScript error if any other function's return value, like y(), is passed.

How can I enforce this type constraint in TypeScript?


Solution

  • If you really really want to do this, you could use a branded type to get something resembling nominal typing. The TypeScript Playground has an example which does just this. Some good use cases for this may be e.g. making a type for separate currencies, a validated UUID or email string, or some other specialization of a primitive type which you want to enforce at the type level.

    You essentially create a type intersection of the type you actually want to use as an argument, but with a unique string (or better, a unique Symbol) as a property. Don't try to actually assign that field, it's there just for the type system. Cast or use a type predicate function to turn a primitive to the branded type.

    type BrandedX = number & {__brand: 'branded x'}
    //                        ^^^^^^^
    // Doesn't have to be `__brand`, could be anything sufficiently obvious.
    
    const x = (): BrandedX => {
      return 1 + 2 as BrandedX
    }
    
    const y = () => {
      return 1 + 2
    }
    
    const fn = (arg: BrandedX) => {
      console.log(arg) // 3
    }
    
    fn(y())
    /* ^^^^
     * Argument of type 'number' is not assignable to parameter of type 'BrandedX'.
     *  Type 'number' is not assignable to type '{ __brand: "branded x"; }
     */
    
    fn(x()) // Works fine
    

    Some TS type libraries offer a helper for this, as well as some validation libraries like Zod.

    If you want to make a helper for it yourself, try something like:

    type Branded<BaseType, Brand extends string> = BaseType & { __brand: Brand }
    // e.g.
    type BrandedX = Branded<number, 'branded x'>
    

    Branded types also combine nicely with type guards:

    type ValidatedEmail = Branded<string, 'validated email'>
    
    const isValidEmail = (value: string): value is ValidatedEmail => {
      // imagine proper validation here
      return value.includes("@")
    }
    
    const sendEmail = (email: ValidatedEmail) => { }
    
    // ...
    
    const input: string = ""
    
    if (!isValidEmail(input)) {
      sendEmail(input) // Error: Argument of type 'string' is not assignable to parameter of type 'ValidatedEmail'.
      return
    }
    
    sendEmail(input) // no error!