Generic functions in TypeScript: 'T' could be instantiated with an arbitrary type which could be unrelated to 'X'

Viewed 639

I have some TypeScript code I'm trying to generalise by introducing generic types for the functions, but TypeScript gives me an error message I'm not able to fully understand.

I have reduced my code down to the following example code, where the comments added to the example hopefully shows my intentions:

/**
 * The types in this example
 */

// A "container" object, which has folders array, 
// which is of either FolderString or FolderNumber.
interface FolderContainer<T extends FolderString | FolderNumber> {
  folders: T[]
}

// Bear with me; the kind of two silly types, just for the sake of the example:
interface FolderString extends FolderContainer<FolderString> {
  age: string
}

interface FolderNumber extends FolderContainer<FolderNumber> {
  age: number
}

/**
 * Utility function for mapping each folder in the data structure,
 * but keep the structure (eg. you cannot map each folder to another type)
 * 
 * I wrote T as T extends {folders: T[]}, as I believe this is enough to describe
 * what the shape of T is in order for the fn to work.
 *
 * I did not think I should set T to for example 
 * T extends FolderString | FolderNumber as that is, as far as I can understand,
 * irrelevant for the mapFoldersKeepStructure fn.
 */
export function mapFoldersKeepStructure<T extends {folders: T[]}>(
  folders: T[],
  mapper: (folder: T, parents: readonly T[]) => T,
  parents: readonly T[] = []
): T[] {
  return folders.map((folder) => {
    const f = mapFoldersKeepStructure(folder.folders, mapper, [...parents, folder])
    return mapper({...folder, folders: f}, parents)
  })
}

/**
 * Two objects which has the shape of the FolderContainer, where it's folders is of the
 * type given
 */
const stringFolders: FolderContainer<FolderString> = {folders: [{age: '13', folders: []}]}
const numberFolders: FolderContainer<FolderNumber> = {folders: [{age: 13, folders: []}]}


// First example of mapFoldersKeepStructure, which works as I expect.
// mapReturn is FolderString[]
const mapReturn = mapFoldersKeepStructure(stringFolders.folders, (folder) => {
  return folder
})

// Second example of usage, now wrapped in someFunction()
function someFunction(folders: FolderNumber[]): FolderNumber[] {
  return mapFoldersKeepStructure(folders, (folder) => {
    return folder
  })
}

// Second example works and returns type as I expect.
// someFunctionReturn is FolderNumber[]
const someFunctionReturn = someFunction(numberFolders.folders)

/**
 * I want to make the previous someFunction() generic. 
 * I thought my first tiny step could be to substitute FolderNumber type with a generic type.
 *
 * I though this change would not do much special, but obviously this is where my 
 * current understanding of TS and generics starts to break down, as TS responds with:
 *
 *  Type '{ folders: T[]; }[]' is not assignable to type 'T[]'.
 *    Type '{ folders: T[]; }' is not assignable to type 'T'.
 *      'T' could be instantiated with an arbitrary type which could be unrelated to '{ folders: T[]; }'.ts(2322)
 */
function someFunctionGeneric<T extends FolderNumber>(folders: T[]): T[] {
  return mapFoldersKeepStructure(folders, (folder) => {
    return folder
  })
}

If you could lead me to, or explain, what piece of the puzzle I'm missing to understand this it would be of great help :-)

0 Answers
Related