Hur man använder Typescript-generics rekursivt?


Jag har ett problem med att använda rekursiva generiska typer.

Jag skapade ett gränssnitt med generics. Idén är att en gren kan ha många undergrenar som också är generics. Detta är min kod (jag har kortat ner den för att posta)

import Contact from 'Business/Models/Contact';
import Office from 'Business/Models/Office';

export interface BranchInterface<T> {
    children: Array<BranchInterface<T>>;
    record: T;
}

export class Branch<T> implements BranchInterface<T> {

    public children: Array<BranchInterface<T>> = [];

    constructor(public record: T) {}
}


const newOffice = new Office();

const rootBranch = new Branch<Office>(newOffice);

rootBranch.children = Array<Branch<Contact>>(); << is the issue

Problemet är att när jag konstruerar en Office-gren, konstrueras barnarrayen som typen Office.

Detta betyder att när jag försöker tilldela en array av Contact-grenar som barn till Office-grenen, kastar TypeScript följande fel;

Type 'Branch<Contact>[]' is not assignable to type 'Branch<Office>[]'...

Anledningen till att göra detta är för att typen av grenar är okänd och jag vill egentligen inte använda any heller om jag kan undvika det.

Så, hur skulle jag lösa detta?

Svar #1

Så, om du verkligen vill starkt typa Branch, behöver du ge det en typ som motsvarar alla nästlade nivåer av children. Det skulle se ut som en lista eller en tupel av typer. Eftersom TypeScript 3.0 introducerade tupltyper i rest/spread-uttryck, kan du på något sätt uttrycka detta, men jag vet inte om det är värt det för dig.

Först låt oss definiera typfunktionerna Head och Tail som delar upp en tupeltyp i dess första element och en tupel av resten av elementen:

type HeadTail<L extends any[]> = 
  ((...args: L) => void) extends ((x: infer H, ...args: infer T) => void) ? [H,T] : never
type Head<L extends any[]> = HeadTail<L>[0];
// e.g., Head<[string, number, boolean]> is string
type Tail<L extends any[]> = HeadTail<L>[1]; 
// e.g., Tail<[string, number, boolean]> is [number, boolean]

Nu kan vi definiera BranchInterface eller Branch för att ta en tupel av typer på det här sättet:

export interface BranchInterface<T extends any[]> {
  children: Array<BranchInterface<Tail<T>>>
  record: Head<T>;
}

export class Branch<T extends any[]> {
  public children: Array<BranchInterface<Tail<T>>> = [];
  constructor(public record: Head<T>) { }
}

Om du antar att du vet att du vill att topnivån ska vara en Office och nästa nivå ner ska vara en Contact, då kan du definiera din lista med typer som [Office, Contact] och se om det fungerar:

const rootBranch = new Branch<[Office, Contact]>(newOffice);
const anOffice = rootBranch.record; // Office
const aContact = rootBranch.children[0].record; // Contact

Självklart, om du går förbi det, får du reda på vad Head<[]> är (den implementationen ger {}, antar jag):

const whoKnows = rootBranch.children[0].children[0].record; // {}

Om du vill att lagren under Contact ska vara något som never istället (eftersom du aldrig kommer att gå ner så långt), kan du använda en resttuplé på detta sätt:

const rootBranch = new Branch<[Office, Contact, ...never[]]>(newOffice);
const anOffice = rootBranch.record; // Office
const aContact = rootBranch.children[0].record; // Contact
const aNever = rootBranch.children[0].children[0].record; // never
const anotherNever = rootBranch.children[0].children[0].children[0].record; // never

Observera att detta kräver att du uttryckligen anger typen T när du skapar en Branch, eftersom kompilatorn inte kan härleda typen från argumentet:

const oops = new Branch(newOffice); 
oops.record; // any, not Office

Tja, det fungerar. Upp till dig om du vill gå den vägen. Hoppas det hjälper; lycka till!

Svar #2

Det verkar som att du kanske använder generiska typer där enkel arv skulle räcka. Om allt du behöver är att ett kontor eller en kontaktgren ska ha en lista över grenbarn, fungerar följande kod med endast en generisk Array.

export class Branch {
  children: Array<Branch>;
  constructor(){
    this.children = new Array<Branch>();
  }
}

class Office extends Branch {}
class Contact extends Branch {}

const newOffice = new Office();
newOffice.children.push(new Contact());

Men om du är fast besluten att använda generics, behöver du 2 generiska typer i din Branch-klass för att separera record- och children-typerna, på följande sätt:

export class Branch<T, U> {

  public children: U[];

  constructor(public record: T) {
    this.children = [];
  }
}

class Office {};
class Contact {};

const newOffice = new Office();

const rootBranch = new Branch<Office, Contact>(newOffice);

rootBranch.children = new Array<Contact>();

Relaterade artiklar