Search code examples
typescripttypescript-typingstypescript-types

How to properly type check nested records allowing partial subtrees with TypeScript?


Please have a look at the following demo.
interface Data defines a schema for nested data.
function check shall validate whether a given partial subtree of this Data structure is fine and throw a compile-time error if not (hopefully with a more or less detailed and understandable error message and not just "... is not assignable to type 'never").

interface Data {
  namespace1: {
    keyA: string,
    keyB: string
  },

  namespace2: {
    keyC: string,
    keyD: string
  }
}

// This function's only purpose is to perform a compile-time check
// whether the given partial data is valid.
// Returns the first and only argument in case of success,
// otherwise a compile-time error will occur.
function check<??>(
  partialData: ????
): ?????? {
  return partialData
}

// Example 1 => okay
const validPartialData1 = check({
  namespace1: {
    keyB: 'b'
  }
})

// Example 2 => okay
const validPartialData2 = check({
  namespace1: {
    keyB: 'b'
  },

  namespace2: {
    keyC: 'c'
  }
})

// Example 3 => okay
const validPartialData3 = check({})

// Example 4 => compile-time error!
const invalidPartialData1 = check({
  namespace1: {
    keyC: 'c'
  }
})

// Example 5 => compile-time error!
const invalidPartialData2 = check({
  xyz: {
    keyA: 'a'
  }
})

Solution

  • You don't need the check function. Use optional field directly.

    interface Data {
      namespace1?: {
        keyA?: string,
        keyB?: string
      },
    
      namespace2?: {
        keyC?: string,
        keyD?: string
      }
    }
    
    const validPartialData1:Data = {
      namespace1: {
        keyB: 'b'
      }
    }
    

    See playground

    If you don't want to change Data type. You can define another PartialData

    type NestPartial<T> = {
        [P in keyof T]?: NestPartial<T[P]>;
    }
    type PartialData = NestPartial<Data>
    
    const validPartialData1: PartialData = {
        namespace1: {
            keyB: 'b'
        }
    }
    

    See playground