typescript - Hur man förstår relationerna mellan typerna any, unknown, {} och mellan dem och andra typer?


Försöker förstå relationer mellan typer har jag denna kod

type CheckIfExtends<A, B> = A extends B ? true : false;

type T1 = CheckIfExtends<number, unknown>; //true
type T2 = CheckIfExtends<number, {}>; //true
type T3 = CheckIfExtends<number, any>; //true
type T4 = CheckIfExtends<() => void, unknown>; //true
type T5 = CheckIfExtends<() => void, {}>; //true
type T6 = CheckIfExtends<() => void, any>; //true
type T7 = CheckIfExtends<unknown, any>; //true
type T8 = CheckIfExtends<any, unknown>; //true
type T9 = CheckIfExtends<{}, unknown>; //true
type T10 = CheckIfExtends<{}, any>; //true
type T11 = CheckIfExtends<any, {}>; //boolean
type T12 = CheckIfExtends<unknown, {}>; //false

Länk till lekplats

Kan någon förklara detta? Vilken skillnad? Hur är det möjligt att any extends {} och any inte extends {} samtidigt? Om any extends unknown och unknown extends any, betyder det då att de är starkt lika? Är det ett nytt fel i Typescript ovanpå null och undefined-likhetsproblemet i JavaScript?

Faktiskt,

type T = CheckIfExtends<any, number>; //boolean

Svar #1

Skillnaden är i huvudsak denna:

  • typen any är medvetet osäker, eftersom den kan tilldelas både till och från vilken annan typ som helst (med möjligt undantag för never, beroende på var du använder den). Osäkerhet betyder att några grundläggande regler för typer bryts, såsom transitivitet av subtypning. Generellt, om A kan tilldelas B, och B kan tilldelas C, då kan A tilldelas C. Men any bryter detta. Till exempel: string kan tilldelas any, och any kan tilldelas number... men string kan inte tilldelas number. Denna särskilda osäkerhet är mycket användbar, eftersom den gör att vi i princip kan "stänga av" typkontroll i en del av koden som antingen är svår eller omöjlig att typa korrekt. Men du måste vara mycket försiktig när du tänker på any som en typ; det är mer som en "icke-typ".

  • den tomma typen, {}, är en typ som kan behandlas som ett objekt vid körning (det vill säga något du kan läsa egenskaper eller metoder från utan ett körfel), men den har inga kända egenskaper vid kompileringstid. Det betyder inte att den saknar egenskaper; det betyder bara att kompilatorn inte känner till några av dem. Detta innebär att endast null och undefined inte kan tilldelas till {} (null.foo eller undefined.foo är körfel). Även primitiva typer som string kan behandlas som om de har egenskaper och metoder vid körning ("".length och "".toUpperCase() fungerar, och även "".foo returnerar bara undefined). Och naturligtvis kan vilken faktisk objekttyp som helst också tilldelas till {}.

    Å andra sidan är typen {} inte tilldelningsbar till särskilt många typer. Om jag har ett värde av typen {} och försöker tilldela det till en variabel av typen {foo: string}, kommer det att uppstå ett kompilatorfel, eftersom {} inte är känt för att innehålla en foo-egenskap. Du kan tilldela {} till sig själv, eller till en bredare typ som unknown, eller till "icke-typen" any.

    Detta gör {} väldigt nästan till en topptyp, vilket är en typ som alla andra typer kan tilldelas. Det är i princip en topptyp med null och undefined borttagna.

  • typen unknown introducerades i TypeScript 3.0 och är den sanna topptypen; varje typ i TypeScript kan tilldelas unknown. Även null och undefined kan tilldelas unknown.

    Återigen, å andra sidan är unknown endast tilldelningsbar till sig själv och den "icke-typen" any. Även typen {} är inte tillräckligt bred för att du ska kunna tilldela unknown till den. Istället är unknown i huvudsak ekvivalent med {} | null | undefined (och betraktas som sådan sedan TypeScript 4.8)


De flesta av dina CheckIfExtends<A, B>-resultat kan förklaras av ovanstående. Undantaget är T11:

type T11 = CheckIfExtends<any, {}>; //boolean

Din CheckIfExtends<A, B>-typdefinition är en distribuerbar villkorstyp, som gör några intressanta saker när A är en unionstyp, eftersom den tillåter båda grenarna av villkoret att väljas om delarna av unionen uppfyller båda grenarna. Den gör också samma distribution när A är any, förutom när B är any eller unknown (så T8 beter sig normalt). Det finns viss diskussion om detta i microsoft/TypeScript#27418. Hur som helst, T11 tar båda grenarna och du får true | false, vilket är boolean. (Från microsoft/TypeScript#27418, unknown i A-positionen distribueras inte, så T7 och T12 beter sig också normalt).

Svar #2

Jag kommer att börja med vad extends betyder i TypeScript. Vid första anblicken ter det sig märkligt, eftersom det för produkttyper (som objekt) beter sig som 'är en övermängd' och för unions som 'är en delmängd', dessutom fungerar det helt annorlunda för funktionstyper. Det kan se märkligt ut vid första ögonblicket, men det är ett logiskt beteende, med andra ord har de flesta typer egenskapen soundness.

Min tumregel för att förstå detta koncept är att läsa extends som kan tilldelas till. Då om x extends y, betyder det att x kan användas närhelst y krävs.

Låt oss överväga tre olika algebraiska datatyper, om det ovanstående gäller för dem.

För produkttyp

type A = {a: string}
type B = {a: string; b: number}
type BextendsA = B extends A ? true : false // evaluates to true 

Ovanstående är sant eftersom B kan användas på alla platser där A krävs, eftersom B täcker hela den struktur som A har. B är en superset av A. Men det som gäller här är att B kan tilldelas A.

För unionstyp

type A = number | string
type B = number
type BextendsA = B extends A ? true : false // evaluates to true 

Totalt annorlunda ser det ut för union. B för produkt var en superset, för union är B en delmängd! Ja, olika logik, B representerar inte alla möjliga värden i A, men närhelst A behövs kan B användas istället. Så B kan tilldelas till A.

För funktionstyper

type A = (a: number) => void
type B = () => void
type BextendsA = B extends A ? true : false // evaluates to true 

För funktionstyper ser det ännu konstigare ut eftersom A ser ut som en mer specificerad funktion än B, så hur kan B då utvidga A? Detta går tillbaka till tilldelningsbarhet, eftersom när något behöver A kan vi tilldela B. Detta är mycket tydligt i exempel som Array.map. Tänk på:

[1,2].map(x => x + 1)

Array.map kräver en funktion som har tre argument - (el,index,arr) => any men kan fungera med en funktion som bara har ett argument el => any. Och återigen gäller tilldelningsbarheten, B kan tilldelas A.

Typer som any, unknown, {} är osäkra, vilket betyder att deras beteende inte kan bevisas logiskt. Att förstå hur de beter sig i TS handlar mer om att förstå specifikationen och resonera kring sådana beslut. Men det kan inte förklaras logiskt, eftersom osäkra typer beter sig mot logik.

De platser där TypeScript tillåter osäkert beteende har noggrant övervägts, och genom hela detta dokument kommer vi att förklara var dessa inträffar och de motiverande scenarierna bakom dem.

Relaterade artiklar