javascript - TypeScript - klasse generisk type er for snæver, hvis den udvider noget andet, er den ikke, hvis den ikke udvider noget


Jeg har klasser som Item<T> og StrictItem<T extends Something>. Hvis jeg initialiserer den førstnævnte som Item('a'), vil den blive set som Item<string>, mens StrictItem<'a'> vil blive set som StrictItem<'a'>.

  1. Hvorfor forårsager det forskelle at sætte en begrænsning på generikken? Det ser ikke ud til at være relateret.
  2. Er der en måde at ændre StrictItem-definitionen, så StrictItem('a') ville være StrictItem<string>, samtidig med at begrænsningen på generikken bevares?

Legeplads

interface PlainObject<T> { [index: string ]: T}

type ItemValue =
  boolean |
  number |
  string |
  ItemValue[] |
  { [index: string ]: ItemValue } |
  null;

class Item<T> {
    private value: T;
    constructor(value: T) {
        this.value = value;
    }
    public set(value: T) {
        this.value = value;
    }
}

// This one puts restrictions on the generic
class StrictItem<T extends ItemValue> extends Item<T> {
}

// 1. Why does the error seen in item2 does not occur here?
let item1 = new Item('a');
item1.set('b');
item1.set(0); // error

// 1. Can StrictItem definition be modified to implictly infer a more
// general type ('string') in this case (without using any type for T)?
// Or is explicitly defining the type of passed value the only way (see item3)?
let item2 = new StrictItem('a');
item2.set('b'); // error

let item3 = new StrictItem('a' as string);
item3.set('b');

Svar #1

Hvorfor forårsager det forskelle at pålægge en begrænsning på generiske typer? Det ser ikke ud til at være relateret.

Jeg tror, at når du gør <T>, fortæller du kompilereren: "brug bare det, der normalt ville blive udledt." Og i let foo='bar' bliver foo udledt som string. Og uden ekstra notation giver dette mening som standard.

Men, <T extends ItemValue> siger noget lidt anderledes. Dette fortæller compileren "T er en type, der udvider ItemValue, hvilket betyder, at det kan være en mere specifik subtype." I dette tilfælde vil den udlede den mest specifikke type, den kan.

Så hvis du begrænser en generisk type, bør du forvente, at det er en meget specifik type. Og normalt er det det, du ønsker.


Er der en måde at ændre StrictItem-definitionen på, så StrictItem('a') ville være StrictItem<string>, samtidig med at begrænsningen på generikken bevares?

Ikke som jeg ved. Men det er normalt faktisk ikke et problem. Det er svært at rådgive en fremgangsmåde ud fra dit konstruerede eksempel, men typisk hardcoder man faktisk ikke en strengliteral ind i en type som denne. I stedet kommer dataene sandsynligvis fra en anden datakilde, funktion eller variabel.

I så fald kan du blot give det en variabel typet som string, og det fungerer som du forventer.

let item4Data = getSomeString() // returns string
let item4 = new StrictItem(item4Data) // StrictItem<string>

Og hvis du virkelig bare vil give et literalt, kan du altid give et typeargument eksplicit, når du bruger typen. Dette er typesikkert, fordi typen af argumentet bliver kontrolleret i forhold til den type, du angiver.

let item5 = new StrictItem<string>('a')
item5.set('b') // works

let item6 = new StrictItem<string>(123) // type error

Svar #2

Her har du en løsning:

interface PlainObject<T> { [index: string]: T }

type ItemValue =
    | boolean
    | number
    | string
    | ItemValue[]
    | { [index: string]: ItemValue }
    | null;

class Item<T> {
    private value: T;
    constructor(value: T) {
        this.value = value;
    }
    public set(value: T) {
        this.value = value;
    }
}

type Infer<T> = T extends infer R ? R : never

// This one puts restrictions on the generic
class StrictItem<T extends ItemValue> extends Item<Infer<T>> { }

let item2 = new StrictItem('a');
item2.set('b'); // ok

Du kan bare bruge infer til at få den mere generelle type og pakke den ind

OPDATERING Som alternativ løsning kan du tilføje en anden generisk parameter med standardværdi:

interface PlainObject<T> { [index: string]: T }

type ItemValue =
    | boolean
    | number
    | string
    | ItemValue[]
    | { [index: string]: ItemValue }
    | null;

class Item<T> {
    private value: T;
    constructor(value: T) {
        this.value = value;
    }
    public set(value: T) {
        this.value = value;
    }
}

type Infer<T> = T extends infer R ? R & ItemValue : never

// This one puts restrictions on the generic
class StrictItem<T, R = Infer<T>> extends Item<R> { }

let item2 = new StrictItem('a');
item2.set('2'); // ok

Relaterede artikler