typescript - varför primitiva typer (t.ex. number) kan tilldelas objekttyper (t.ex. Number) men inte tvärtom?
Kort version:
Varför tillåter TypeScript följande
let num: Number = new Number(1);
num = 1;
Men tillåter inte detta
let num: number = 1;
num = new Number(1)
Det är välkänt att JS har två strängliknande, sifferliknande värden.
let primNum = 1;
let objNum = new Number(1);
let primString = "str";
let objString = new String("str");
Och alla råds att inte förväxla dessa, och starkt föredra de primitiva versionerna.
TypeScript är den statiskt typade superset av JavaScript som försöker minimera typfel. Om det finns värden som man kan förväxla och operationen leder till osäkert resultat visar TS felet vid kompilering. Till exempel, överväg skillnaden mellan 1 + 1 + "1" och "1" + 1 + 1. I JavaScript utvärderas det första värdet till 21 och det andra till 111. Ur JS:s perspektiv kan du lägga till vilka två värden som helst. Du kan lägga till två strängar, två nummer, ett nummer och en sträng och så vidare utan problem. Detta beteende är dock förvirrande och TypeScript eliminerar den möjliga förvirringen genom att vägra kompilera när operanderna av + inte är båda strängar eller båda nummer. En viktig punkt är att även om viss JS-kod där ute kan förvänta sig att kunna lägga till vilka två saker som helst eller att en sådan förväntning kan vara ovanlig spelar det ingen roll; TS sätter gränsen och tillåter inte en sådan operation.
Ändå har TypeScript inga betänkligheter med följande,
let num: Number = new Number(1);
num = 1;
Först deklarerar vi num som en objekttyp (i TypeScript-termer en Number) men vi tilldelar senare den variabeln en primitiv typ (i TS-termer en number). Detta är en förvirring som borde fångas av TS men TS tillåter det även utan en varning. Så man kan hypotetisera att TS inte gör någon åtskillnad mellan Number och number. (vi kanske inte gillar beslutet att inte skilja dem åt men åtminstone i det fallet vet vi vad vi arbetar med)
Men det är inte korrekt heller. TS ger korrekt fel för följande:
let num: number = 1;
num = new Number(1)
Vilket betyder att man kan tilldela ett number till ett Number men inte tvärtom? Så, huvudfrågan är att om TS kan skilja mellan number och Number, varför tillåter det att man tilldelar ett Number till ett number. Varför tillåter man inte båda tilldelningarna?
Är detta ett fel? Om inte, vad är designbeslutet bakom detta?
TS har för närvarande ingen specifikation men en gammal arkiverad säger att
För att bestämma subtyp-, supertyp- och tilldelningskompatibilitetsrelationer behandlas de primitiva typerna Number, Boolean och String som objekttyper med samma egenskaper som gränssnitten 'Number', 'Boolean' respektive 'String'.
Om detta stämmer borde vi kunna tilldela ett nummer till ett nummer (vilket vi kan), men vi borde också kunna göra tvärtom. Vilket betyder att detta gamla dokument inte hänger ihop med hur TS fungerar idag. Frågan är vad den nuvarande faktiska regeln är som bestämmer subtyprelationen mellan primitiva och objekttyper.
Man kan vara oenig med en sådan regel baserat på idén att vad det än är så eliminerar det inte förvirringen mellan number och Number, men det är inte poängen. Poängen är att det inte ens är klart vad den regeln är.
Huvudfrågan: vad är regeln som avgör när en primitiv typ är en underordnad typ av en annan typ?
Liknande frågor har ställts på StackOverflow men alla svar förklarar skillnaden mellan nummer-primitiva typer och nummerobjekttyper, och rekommenderar att man undviker objektversionen. Men här handlar frågan inte om JavaScript-värden utan om TypeScripts typystem. Frågan är på vilken grund de primitiva typerna och objekttyperna jämförs? och denna fråga är inte en kopia av någon som har besvarats.
Svar #1
Viktig notering:
Använd inte Number-konstruktorn. Det som följer antar att du bortser från det mycket goda rådet.
Jag tror att du frågar varför detta är tillåtet:
const bigN: Number = 123 // fine
När detta inte är:
const smallN: number = new Number() // error
Och du förväntar dig att båda skulle vara ett fel.
Svaret är att du kan använda en number som en Number utan att någonsin märka skillnaden. Men du kan inte använda en Number som en number utan att ändra hur number fungerar.
Titta på denna notering på MDN om Number-konstruktorn
När
Numberanropas som en konstruktor (mednew), skapar det ettNumber-objekt, som inte är en primitiv. Till exempel,typeof new Number(42) === "object", ochnew Number(42) !== 42(även om newNumber(42) == 42).Varning: Du bör sällan använda
Numbersom en konstruktor.
Till exempel, notera att:
new Number(42) !== 42
Så :
const n1 = new Number(123)
const n2 = new Number(123)
if (n1 === n2) {
doSomethingImportant()
// expected this to run, never does.
}
Man förväntar sig att två identiska nummer är lika enligt ===-operatorn, men om åtminstone ett är ett Number-objekt, är det inte sant, och din kod går förmodligen sönder någonstans. Så det är värt att skydda sig mot.
Å andra sidan, låt oss säga att du faktiskt förväntade dig det:
const n1: Number = 123
const n2: Number = 123
if (n1 === n2) {
throw new Error('never expected this to happen!')
// expected this NOT to run, but it does
}
Nu skulle du kunna hävda att det är precis lika fel. Men här är grejen:
Mängden kod i världen som kontrollerar numerisk värdeekvivalens överskuggas enormt av mängden kod som förväntar sig att två Number-objekt inte har samma identitet.
Detta betyder att detta är astronomiskt mindre sannolikt att vara ett fel.
Och detta är varför number kan tilldelas Number.
Som med många saker i Typescript, tror jag att detta är en avvägning mellan renhet och pragmatism. Och den stora majoriteten av tiden kommer användning av en number som Number inte att orsaka några problem, medan att gå åt andra hållet kommer att bryta åtminstone vissa mycket viktiga beteenden i hur number förväntas bete sig.
