Utforska typsystemet
TypeScript-språktjänsten
TypeScript-språktjänsten, även känd som tsserver, erbjuder olika funktioner såsom felrapportering, diagnostik, kompilera-vid-sparning, namnbyte, gå till definition, kompletteringslistor, signaturhjälp och mer. Den används främst av integrerade utvecklingsmiljöer (IDE:er ) för att ge IntelliSense-stöd. Den integreras sömlöst med Visual Studio Code och används av verktyg som Conquer of Completion (Coc).
Utvecklare kan utnyttja ett dedikerat API och skapa sina egna anpassade språktjänstplugin för att förbättra TypeScript-redigeringsupplevelsen. Detta kan vara särskilt användbart för att implementera speciella linting-funktioner eller möjliggöra automatisk komplettering för ett anpassat mallspråk.
Ett exempel på ett verkligt anpassat plugin är “typescript-styled-plugin”, som tillhandahåller syntaxfelrapportering och IntelliSense-stöd för CSS-egenskaper i styled components.
För mer information och snabbstartsguider kan du hänvisa till den officiella TypeScript-wikin på GitHub: https://github.com/microsoft/TypeScript/wiki/
Strukturell typning
TypeScript är baserat på ett strukturellt typsystem. Detta innebär att kompatibiliteten och ekvivalensen hos typer bestäms av typens faktiska struktur eller definition, snarare än dess namn eller plats för deklaration, som i nominativa typsystem som C# eller C.
TypeScripts strukturella typsystem designades baserat på hur JavaScripts dynamiska duck typing-system fungerar vid körning.
Följande exempel är giltig TypeScript-kod. Som du kan observera har “X” och “Y” samma medlem “a”, även om de har olika deklarationsnamn. Typerna bestäms av deras strukturer, och i detta fall, eftersom strukturerna är desamma, är de kompatibla och giltiga.
type X = { a: string;};type Y = { a: string;};const x: X = { a: 'a' };const y: Y = x; // GiltigtGrundläggande jämförelseregler i TypeScript
TypeScripts jämförelseprocess är rekursiv och utförs på typer som är nästlade på valfri nivå.
En typ “X” är kompatibel med “Y” om “Y” har åtminstone samma medlemmar som “X”.
type X = { a: string;};const y = { a: 'A', b: 'B' }; // Giltigt, eftersom det har åtminstone samma medlemmar som Xconst r: X = y;Funktionsparametrar jämförs efter typer, inte efter deras namn:
type X = (a: number) => void;type Y = (a: number) => void;let x: X = (j: number) => undefined;let y: Y = (k: number) => undefined;y = x; // Giltigtx = y; // GiltigtFunktionens returtyper måste vara desamma:
type X = (a: number) => undefined;type Y = (a: number) => number;let x: X = (a: number) => undefined;let y: Y = (a: number) => 1;y = x; // Ogiltigtx = y; // OgiltigtReturtypen för en källfunktion måste vara en undertyp av returtypen för en målfunktion:
let x = () => ({ a: 'A' });let y = () => ({ a: 'A', b: 'B' });x = y; // Giltigty = x; // Ogiltigt, medlemmen b saknasAtt utelämna funktionsparametrar är tillåtet, eftersom det är vanlig praxis i JavaScript, till exempel vid användning av “Array.prototype.map()”:
[1, 2, 3].map((element, _index, _array) => element + 'x');Därför är följande typdeklarationer helt giltiga:
type X = (a: number) => undefined;type Y = (a: number, b: number) => undefined;let x: X = (a: number) => undefined;let y: Y = (a: number) => undefined; // Parametern b saknasy = x; // GiltigtEventuella ytterligare valfria parametrar i källtypen är giltiga:
type X = (a: number, b?: number, c?: number) => undefined;type Y = (a: number) => undefined;let x: X = a => undefined;let y: Y = a => undefined;y = x; // Giltigtx = y; //GiltigtEventuella valfria parametrar i måltypen utan motsvarande parametrar i källtypen är giltiga och utgör inte ett fel:
type X = (a: number) => undefined;type Y = (a: number, b?: number) => undefined;let x: X = a => undefined;let y: Y = a => undefined;y = x; // Giltigtx = y; // GiltigtRest-parametern behandlas som en oändlig serie av valfria parametrar:
type X = (a: number, ...rest: number[]) => undefined;let x: X = a => undefined; //giltigtFunktioner med överlagringar är giltiga om överlagringssignaturen är kompatibel med dess implementeringssignatur:
function x(a: string): void;function x(a: string, b: number): void;function x(a: string, b?: number): void { console.log(a, b);}x('a'); // Giltigtx('a', 1); // Giltigt
function y(a: string): void; // Ogiltigt, inte kompatibelt med implementationssignaturenfunction y(a: string, b: number): void;function y(a: string, b: number): void { console.log(a, b);}y('a');y('a', 1);Jämförelse av funktionsparametrar lyckas om käll- och målparametrarna kan tilldelas supertyper eller undertyper (bivarians).
// Supertypclass X { a: string; constructor(value: string) { this.a = value; }}// Undertypclass Y extends X {}// Undertypclass Z extends X {}
type GetA = (x: X) => string;const getA: GetA = x => x.a;
// Bivarians accepterar supertyperconsole.log(getA(new X('x'))); // Giltigtconsole.log(getA(new Y('Y'))); // Giltigtconsole.log(getA(new Z('z'))); // GiltigtEnums är jämförbara och giltiga med tal och vice versa, men att jämföra Enum-värden från olika Enum-typer är ogiltigt.
enum X { A, B,}enum Y { A, B, C,}const xa: number = X.A; // Giltigtconst ya: Y = 0; // GiltigtX.A === Y.A; // OgiltigtInstanser av en klass genomgår en kompatibilitetskontroll för sina privata och skyddade medlemmar:
class X { public a: string; constructor(value: string) { this.a = value; }}
class Y { private a: string; constructor(value: string) { this.a = value; }}
let x: X = new Y('y'); // OgiltigtJämförelsekontrollen tar inte hänsyn till den olika arvshierarkin, till exempel:
class X { public a: string; constructor(value: string) { this.a = value; }}class Y extends X { public a: string; constructor(value: string) { super(value); this.a = value; }}class Z { public a: string; constructor(value: string) { this.a = value; }}let x: X = new X('x');let y: Y = new Y('y');let z: Z = new Z('z');x === y; // Giltigtx === z; // Giltigt även om z tillhör en annan arvshierarkiGenerics jämförs med hjälp av deras strukturer baserat på den resulterande typen efter tillämpning av den generiska parametern. Bara slutresultatet jämförs som en icke-generisk typ.
interface X<T> { a: T;}let x: X<number> = { a: 1 };let y: X<string> = { a: 'a' };x === y; // Ogiltigt eftersom typargumentet används i den slutliga struktureninterface X<T> {}const x: X<number> = 1;const y: X<string> = 'a';x === y; // Giltigt eftersom typargumentet inte används i den slutliga strukturenNär generics inte har sitt typargument specificerat behandlas alla ospecificerade argument som typer med “any”:
type X = <T>(x: T) => T;type Y = <K>(y: K) => K;let x: X = x => x;let y: Y = y => y;x = y; // GiltigtKom ihåg:
let a: number = 1;let b: number = 2;a = b; // Giltigt, allt kan tilldelas sig självt
let c: any;c = 1; // Giltigt, alla typer kan tilldelas any
let d: unknown;d = 1; // Giltigt, alla typer kan tilldelas unknown
let e: unknown;let e1: unknown = e; // Giltigt, unknown kan endast tilldelas sig självt och anylet e2: any = e; // Giltigtlet e3: number = e; // Ogiltigt
let f: never;f = 1; // Ogiltigt, inget kan tilldelas never
let g: void;let g1: any;g = 1; // Ogiltigt, void kan varken tilldelas eller ta emot något annat än anyg = g1; // GiltigtObservera att när “strictNullChecks” är aktiverat behandlas “null” och “undefined” på liknande sätt som “void”; annars liknar de “never”.
Typer som mängder
I TypeScript är en typ en mängd av möjliga värden. Denna mängd kallas även typens domän. Varje värde av en typ kan ses som ett element i en mängd. En typ fastställer de begränsningar som varje element i mängden måste uppfylla för att betraktas som en medlem av den mängden. TypeScripts primära uppgift är att kontrollera och verifiera om en mängd är en delmängd av en annan.
TypeScript stöder olika typer av mängder:
| Mängdterm | TypeScript | Anteckningar |
|---|---|---|
| Tom mängd | never | ”never” innehåller ingenting förutom sig själv |
| Enelement-mängd | undefined / null / literal type | |
| Ändlig mängd | boolean / union | |
| Oändlig mängd | string / number / object | |
| Universell mängd | any / unknown | Varje element är medlem i “any” och varje mängd är en delmängd av den / “unknown” är en typsäker motsvarighet till “any” |
Här är några exempel:
| TypeScript | Mängdterm | Exempel |
|---|---|---|
| never | ∅ (tom mängd) | const x: never = ‘x’; // Error: Type ‘string’ is not assignable to type ‘never’ |
| Literal type | Enelement-mängd | type X = ‘X’; |
| type Y = 7; | ||
| Värde tilldelbart till T | Värde ∈ T (medlem av) | type XY = ‘X’ | ‘Y’; |
| const x: XY = ‘X’; | ||
| T1 tilldelbart till T2 | T1 ⊆ T2 (delmängd av) | type XY = ‘X’ | ‘Y’; |
| const x: XY = ‘X’; | ||
| const j: XY = ‘J’; // Type ‘“J”’ is not assignable to type ‘XY’. | ||
| T1 extends T2 | T1 ⊆ T2 (delmängd av) | type X = ‘X’ extends string ? true : false; |
| T1 | T2 | T1 ∪ T2 (union) | type XY = ‘X’ | ‘Y’; |
| type JK = 1 | 2; | ||
| T1 & T2 | T1 ∩ T2 (snitt) | type X = { a: string } |
| type Y = { b: string } | ||
| type XY = X & Y | ||
| const x: XY = { a: ‘a’, b: ‘b’ } | ||
| unknown | Universell mängd | const x: unknown = 1 |
En union, (T1 | T2) skapar en bredare mängd (båda):
type X = { a: string;};type Y = { b: string;};type XY = X | Y;const r: XY = { a: 'a', b: 'x' }; // GiltigtEn intersektion, (T1 & T2) skapar en smalare mängd (endast delade):
type X = { a: string;};type Y = { a: string; b: string;};type XY = X & Y;const r: XY = { a: 'a' }; // Ogiltigtconst j: XY = { a: 'a', b: 'b' }; // GiltigtNyckelordet extends kan betraktas som “delmängd av” i detta sammanhang. Det anger en begränsning för en typ. När extends används med en generisk typ begränsar det den generiska typparametern till en mer specifik typ.
Observera att extends här inte har något att göra med klassarv i OOP-sammanhang.
TypeScript arbetar med strukturella typer och har inte en strikt nominell hierarki. Faktum är att, som i exemplet nedan, två typer kan överlappa utan att någon av dem är en subtyp av den andra, eftersom TypeScript tar hänsyn till strukturen, eller formen, hos objekt.
interface X { a: string;}interface Y extends X { b: string;}interface Z extends Y { c: string;}const z: Z = { a: 'a', b: 'b', c: 'c' };interface X1 { a: string;}interface Y1 { a: string; b: string;}interface Z1 { a: string; b: string; c: string;}const z1: Z1 = { a: 'a', b: 'b', c: 'c' };
const r: Z1 = z; // GiltigtTilldela en typ: Typdeklarationer och Typpåståenden
En typ kan tilldelas på olika sätt i TypeScript:
Typdeklaration
I följande exempel använder vi x: X (”: Type”) för att deklarera en typ för variabeln x.
type X = { a: string;};
// Typdeklarationconst x: X = { a: 'a',};Om variabeln inte har det angivna formatet kommer TypeScript att rapportera ett fel. Till exempel:
type X = { a: string;};
const x: X = { a: 'a', b: 'b', // Fel: En objektliteral får endast ange kända egenskaper};Typpåstående
Det är möjligt att lägga till ett påstående genom att använda nyckelordet as. Detta talar om för kompilatorn att utvecklaren har mer information om en typ och tystar eventuella fel som kan uppstå.
Till exempel:
type X = { a: string;};const x = { a: 'a', b: 'b',} as X;I exemplet ovan påstås objektet x ha typen X med hjälp av nyckelordet as. Detta informerar TypeScript-kompilatorn om att objektet överensstämmer med den angivna typen, även om det har en extra egenskap b som inte finns i typdefinitionen.
Typpåståenden är användbara i situationer där en mer specifik typ behöver anges, särskilt vid arbete med DOM:en . Till exempel:
const myInput = document.getElementById('my_input') as HTMLInputElement;Här används typpåståendet as HTMLInputElement för att tala om för TypeScript att resultatet av getElementById ska behandlas som ett HTMLInputElement. Typpåståenden kan också användas för att mappa om nycklar, som visas i exemplet nedan med malliteraler:
type J<Type> = { [ Property in keyof Type as `prefix_${string & Property}` ]: () => Type[Property];};type X = { a: string; b: number;};type Y = J<X>;I detta exempel använder typen J<Type> en mappad typ med en malliteral för att mappa om nycklarna i Type. Den skapar nya egenskaper med ett “prefix_” tillagt till varje nyckel, och deras motsvarande värden är funktioner som returnerar de ursprungliga egenskapsvärdena.
Det är värt att notera att när man använder ett typpåstående kommer TypeScript inte att utföra kontroll av överskottsegenskaper. Därför är det generellt att föredra att använda en typdeklaration när objektets struktur är känd i förväg.
Omgivande deklarationer
Omgivande deklarationer är filer som beskriver typer för JavaScript-kod. De har filnamnsformatet .d.ts. De importeras vanligtvis och används för att annotera befintliga JavaScript-bibliotek eller för att lägga till typer till befintliga JS-filer i ditt projekt.
Många vanliga bibliotekstyper finns på: https://github.com/DefinitelyTyped/DefinitelyTyped/
och kan installeras med:
npm install --save-dev @types/library-nameFör dina egna omgivande deklarationer kan du importera dem med “triple-slash”-referensen:
/// <reference path="./library-types.d.ts" />Du kan använda omgivande deklarationer även i JavaScript-filer med // @ts-check.
Nyckelordet declare möjliggör typdefinitioner för befintlig JavaScript-kod utan att importera den, och fungerar som en platshållare för typer från en annan fil eller globalt.
Egenskapskontroll och kontroll av överskottsegenskaper
TypeScript bygger på ett strukturellt typsystem, men kontroll av överskottsegenskaper är en egenskap hos TypeScript som gör det möjligt att kontrollera om ett objekt har exakt de egenskaper som anges i typen.
Kontroll av överskottsegenskaper utförs vid tilldelning av objektliteraler till variabler eller när de skickas som argument till funktionens överskottsegenskap, till exempel.
type X = { a: string;};const y = { a: 'a', b: 'b' };const x: X = y; // Giltigt på grund av strukturell typningconst w: X = { a: 'a', b: 'b' }; // Ogiltigt på grund av kontrollen av överflödiga egenskaperSvaga typer
En typ anses vara svag när den inte innehåller annat än en uppsättning helt valfria egenskaper:
type X = { a?: string; b?: string;};TypeScript betraktar det som ett fel att tilldela något till en svag typ när det inte finns någon överlappning. Till exempel ger följande ett fel:
type Options = { a?: string; b?: string;};
const fn = (options: Options) => undefined;
fn({ c: 'c' }); // OgiltigtÄven om det inte rekommenderas, är det möjligt att kringgå denna kontroll genom att använda typpåstående om det behövs:
type Options = { a?: string; b?: string;};const fn = (options: Options) => undefined;fn({ c: 'c' } as Options); // GiltigtEller genom att lägga till unknown i indexsignaturen till den svaga typen:
type Options = { [prop: string]: unknown; a?: string; b?: string;};
const fn = (options: Options) => undefined;fn({ c: 'c' }); // GiltigtStrikt kontroll av objektliteraler (Freshness)
Strikt kontroll av objektliteraler, ibland kallad “freshness”, är en funktion i TypeScript som hjälper till att fånga överskotts- eller felstavade egenskaper som annars skulle gå obemärkta vid normala strukturella typkontroller.
När man skapar en objektliteral betraktar TypeScript-kompilatorn den som “fresh”. Om objektliteralen tilldelas till en variabel eller skickas som parameter kommer TypeScript att ge ett fel om objektliteralen anger egenskaper som inte finns i måltypen.
Dock försvinner “freshness” när en objektliteral breddas eller ett typpåstående används.
Här är några exempel för att illustrera:
type X = { a: string };type Y = { a: string; b: string };
let x: X;x = { a: 'a', b: 'b' }; // Färskhetskontroll: Ogiltig tilldelningvar y: Y;y = { a: 'a', bx: 'bx' }; // Färskhetskontroll: Ogiltig tilldelning
const fn = (x: X) => console.log(x.a);
fn(x);fn(y); // Typbreddning: Inga fel, strukturellt typkompatibel
fn({ a: 'a', bx: 'b' }); // Färskhetskontroll: Ogiltigt argument
let c: X = { a: 'a' };let d: Y = { a: 'a', b: '' };c = d; // Typbreddning: Ingen färskhetskontrollTypinferens
TypeScript kan härleda typer när ingen annotering tillhandahålls vid:
- Variabelinitiering.
- Medlemsinitiering.
- Inställning av standardvärden för parametrar.
- Funktionens returtyp.
Till exempel:
let x = 'x'; // Den härledda typen är stringTypeScript-kompilatorn analyserar värdet eller uttrycket och bestämmer dess typ baserat på tillgänglig information.
Mer avancerade inferenser
När flera uttryck används vid typinferens letar TypeScript efter de “bästa gemensamma typerna”. Till exempel:
let x = [1, 'x', 1, null]; // Den härledda typen är: (string | number | null)[]Om kompilatorn inte kan hitta de bästa gemensamma typerna returnerar den en unionstyp. Till exempel:
let x = [new RegExp('x'), new Date()]; // Den härledda typen är: (RegExp | Date)[]TypeScript använder “kontextuell typning” baserat på variabelns placering för att härleda typer. I följande exempel vet kompilatorn att e är av typen MouseEvent på grund av händelsetypen click som definieras i filen lib.d.ts, vilken innehåller omgivande deklarationer för olika vanliga JavaScript-konstruktioner och DOM:en:
window.addEventListener('click', function (e) {}); // Den härledda typen för e är MouseEventTypbreddning
Typbreddning är den process där TypeScript tilldelar en typ till en variabel som initierats utan att en typannotering angavs. Den tillåter övergång från smal till bredare typ men inte tvärtom. I följande exempel:
let x = 'x'; // TypeScript härleder string, en bred typlet y: 'y' | 'x' = 'y'; // Typen för y är en union av literaltypery = x; // Ogiltigt: Typen 'string' kan inte tilldelas typen '"x" | "y"'.TypeScript tilldelar string till x baserat på det enda värde som angavs vid initieringen (x), detta är ett exempel på breddning.
TypeScript tillhandahåller sätt att kontrollera breddningsprocessen, till exempel genom att använda “const”.
Const
Att använda nyckelordet const vid deklaration av en variabel resulterar i en smalare typinferens i TypeScript.
Till exempel:
const x = 'x'; // TypeScript härleder typen för x som 'x', en snävare typlet y: 'y' | 'x' = 'y';y = x; // Giltigt: Typen för x härleds som 'x'Genom att använda const för att deklarera variabeln x, smalnas dess typ av till det specifika literalvärdet ‘x’. Eftersom typen av x är avsmalnad kan den tilldelas till variabeln y utan något fel.
Anledningen till att typen kan härledas är att const-variabler inte kan omtilldelas, så deras typ kan smalnas av till en specifik literaltyp, i detta fall literaltypen ‘x’.
Const-modifierare på typparametrar
Från version 5.0 av TypeScript är det möjligt att ange attributet const på en generisk typparameter. Detta möjliggör härledning av den mest precisa typen möjligt. Låt oss se ett exempel utan att använda const:
function identity<T>(value: T) { // Inget const här return value;}const values = identity({ a: 'a', b: 'b' }); // Den härledda typen är: { a: string; b: string; }Som du kan se härleddes egenskaperna a och b med typen string.
Låt oss nu se skillnaden med const-versionen:
function identity<const T>(value: T) { // Använder modifieraren const på typparametrar return value;}const values = identity({ a: 'a', b: 'b' }); // Den härledda typen är: { a: "a"; b: "b"; }Nu kan vi se att egenskaperna a och b härleddes som const, så a och b behandlas som strängliteraler snarare än bara string-typer.
Const-påstående
Denna funktion låter dig deklarera en variabel med en mer precis literaltyp baserat på dess initieringsvärde, och signalerar till kompilatorn att värdet ska behandlas som en oföränderlig literal. Här är några exempel:
På en enskild egenskap:
const v = { x: 3 as const,};v.x = 3;På ett helt objekt:
const v = { x: 1, y: 2,} as const;Detta kan vara särskilt användbart vid definition av typen för en tupel:
const x = [1, 2, 3]; // number[]const y = [1, 2, 3] as const; // Tupel readonly [1, 2, 3]Explicit typannotering
Vi kan vara specifika och ange en typ. I följande exempel är egenskapen x av typen number:
const v = { x: 1, // Härledd typ: number (typbreddning)};v.x = 3; // GiltigtVi kan göra typannoteringen mer specifik genom att använda en union av literaltyper:
const v: { x: 1 | 2 | 3 } = { x: 1, // x är nu en union av literaltyper: 1 | 2 | 3};v.x = 3; // Giltigtv.x = 100; // OgiltigtTypavsmalnande
Typavsmalnande är den process i TypeScript där en generell typ smalnas av till en mer specifik typ. Detta sker när TypeScript analyserar koden och avgör att vissa villkor eller operationer kan förfina typinformationen.
Avsmalnande av typer kan ske på olika sätt, bland annat:
Villkor
Genom att använda villkorssatser, som if eller switch, kan TypeScript smalna av typen baserat på utfallet av villkoret. Till exempel:
let x: number | undefined = 10;
if (x !== undefined) { x += 100; // Typen är number, som har snävats in av villkoret}Kasta eller returnera
Att kasta ett fel eller returnera tidigt från en gren kan användas för att hjälpa TypeScript smalna av en typ. Till exempel:
let x: number | undefined = 10;
if (x === undefined) { throw 'error';}x += 100;Andra sätt att smalna av typer i TypeScript inkluderar:
instanceof-operatorn: Används för att kontrollera om ett objekt är en instans av en specifik klass.in-operatorn: Används för att kontrollera om en egenskap finns i ett objekt.typeof-operatorn: Används för att kontrollera typen av ett värde vid körning.- Inbyggda funktioner som
Array.isArray(): Används för att kontrollera om ett värde är en array.
Diskriminerad union
Att använda en “diskriminerad union” är ett mönster i TypeScript där en explicit “tagg” läggs till objekt för att skilja mellan olika typer inom en union. Detta mönster kallas också en “taggad union”. I följande exempel representeras “taggen” av egenskapen “type”:
type A = { type: 'type_a'; value: number };type B = { type: 'type_b'; value: string };
const x = (input: A | B): string | number => { switch (input.type) { case 'type_a': return input.value + 100; // typen är A case 'type_b': return input.value + 'extra'; // typen är B }};Användardefinierade typvakter
I fall där TypeScript inte kan avgöra en typ är det möjligt att skriva en hjälpfunktion känd som en “användardefinierad typvakt”. I följande exempel kommer vi att använda ett typpredikat för att smalna av typen efter att viss filtrering har tillämpats:
const data = ['a', null, 'c', 'd', null, 'f'];
const r1 = data.filter(x => x != null); // Typen är (string | null)[], TypeScript kunde inte härleda typen korrekt
const isValid = (item: string | null): item is string => item !== null; // Anpassad typvakt
const r2 = data.filter(isValid); // Typen är nu korrekt string[], med hjälp av typvakten med predikat kunde vi snäva in typenSwitch-true-förträngning
TypeScript 5.3 lägger till switch-true-förträngning, vilket låter dig ersätta röriga if/else-kedjor med switch (true) med hjälp av booleska villkor. Det förbättrar läsbarheten och förtränger fortfarande typer. Det liknar mönstermatchning, men enklare.
function classify(x: unknown) { switch (true) { case typeof x === 'string': return `"${x.toUpperCase()}"`; case typeof x === 'number': return x > 0 ? 'positive' : 'negative'; case Array.isArray(x): return `[${x.length} items]`; default: return 'something else'; }}