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
) 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; // ValidGrundlä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' }; // Valid, as it has at least the same members as 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; // Validx = y; // ValidFunktionens 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; // Invalidx = y; // InvalidReturtypen 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; // Validy = x; // Invalid member b is missingAtt 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; // Missing b parametery = x; // ValidEventuella 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; // Validx = y; //ValidEventuella 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; // Validx = y; // ValidRest-parametern behandlas som en oändlig serie av valfria parametrar:
type X = (a: number, ...rest: number[]) => undefined;let x: X = a => undefined; //validFunktioner 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'); // Validx('a', 1); // Valid
function y(a: string): void; // Invalid, not compatible with implementation signaturefunction 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).
// Supertypeclass X { a: string; constructor(value: string) { this.a = value; }}// Subtypeclass Y extends X {}// Subtypeclass Z extends X {}
type GetA = (x: X) => string;const getA: GetA = x => x.a;
// Bivariance does accept supertypesconsole.log(getA(new X('x'))); // Validconsole.log(getA(new Y('Y'))); // Validconsole.log(getA(new Z('z'))); // ValidEnums ä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; // Validconst ya: Y = 0; // ValidX.A === Y.A; // InvalidInstanser 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'); // InvalidJä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; // Validx === z; // Valid even if z is from a different inheritance hierarchyGenerics 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; // Invalid as the type argument is used in the final structureinterface X<T> {}const x: X<number> = 1;const y: X<string> = 'a';x === y; // Valid as the type argument is not used in the final structureNä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; // ValidKom ihåg:
let a: number = 1;let b: number = 2;a = b; // Valid, everything is assignable to itself
let c: any;c = 1; // Valid, all types are assignable to any
let d: unknown;d = 1; // Valid, all types are assignable to unknown
let e: unknown;let e1: unknown = e; // Valid, unknown is only assignable to itself and anylet e2: any = e; // Validlet e3: number = e; // Invalid
let f: never;f = 1; // Invalid, nothing is assignable to never
let g: void;let g1: any;g = 1; // Invalid, void is not assignable to or from anything expect anyg = g1; // ValidObservera 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' }; // ValidEn 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' }; // Invalidconst j: XY = { a: 'a', b: 'b' }; // ValidNyckelordet extends kan betraktas som “delmängd av” i detta sammanhang. Det sätter en begränsning för en typ. När extends används med en generisk typ, behandlas den generiska typen som en oändlig mängd och begränsas till en mer specifik typ.
Observera att extends inte har något att göra med hierarki i OOP-bemärkelse (det finns inget sådant koncept i TypeScript).
TypeScript arbetar med mängder och har ingen strikt hierarki. Faktum är att, som i exemplet nedan, två typer kan överlappa utan att någon av dem är en undertyp av den andra (TypeScript betraktar strukturen, formen på objekten).
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; // ValidTilldela 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;};
// Type declarationconst 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', // Error: Object literal may only specify known properties};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
. 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; // Valid because structural typingconst w: X = { a: 'a', b: 'b' }; // Invalid because excess property checkingSvaga 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' }); // InvalidÄ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); // ValidEller 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' }); // ValidStrikt 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' }; // Freshness check: Invalid assignmentvar y: Y;y = { a: 'a', bx: 'bx' }; // Freshness check: Invalid assignment
const fn = (x: X) => console.log(x.a);
fn(x);fn(y); // Widening: No errors, structurally type compatible
fn({ a: 'a', bx: 'b' }); // Freshness check: Invalid argument
let c: X = { a: 'a' };let d: Y = { a: 'a', b: '' };c = d; // Widening: No Freshness checkTypinferens
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'; // The type inferred is 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]; // The type inferred is: (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()]; // Type inferred is: (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) {}); // The inferred type of e is 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 infers as string, a wide typelet y: 'y' | 'x' = 'y'; // y types is a union of literal typesy = x; // Invalid Type 'string' is not assignable to type '"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 infers the type of x as 'x', a narrower typelet y: 'y' | 'x' = 'y';y = x; // Valid: The type of x is inferred as '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) { // No const here return value;}const values = identity({ a: 'a', b: 'b' }); // Type infered is: { 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) { // Using const modifier on type parameters return value;}const values = identity({ a: 'a', b: 'b' }); // Type infered is: { 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; // Tuple of 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, // Inferred type: number (widening)};v.x = 3; // ValidVi kan göra typannoteringen mer specifik genom att använda en union av literaltyper:
const v: { x: 1 | 2 | 3 } = { x: 1, // x is now a union of literal types: 1 | 2 | 3};v.x = 3; // Validv.x = 100; // InvalidTypavsmalnande
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; // The type is number, which had been narrowed by the condition}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; // type is A case 'type_b': return input.value + 'extra'; // type is 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); // The type is (string | null)[], TypeScript was not able to infer the type properly
const isValid = (item: string | null): item is string => item !== null; // Custom type guard
const r2 = data.filter(isValid); // The type is fine now string[], by using the predicate type guard we were able to narrow the type