Hoppa till innehåll

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; // Giltigt

Grundlä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 X
const 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; // Giltigt
x = y; // Giltigt

Funktionens 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; // Ogiltigt
x = y; // Ogiltigt

Returtypen 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; // Giltigt
y = x; // Ogiltigt, medlemmen b saknas

Att 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 saknas
y = x; // Giltigt

Eventuella 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; // Giltigt
x = y; //Giltigt

Eventuella 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; // Giltigt
x = y; // Giltigt

Rest-parametern behandlas som en oändlig serie av valfria parametrar:

type X = (a: number, ...rest: number[]) => undefined;
let x: X = a => undefined; //giltigt

Funktioner 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'); // Giltigt
x('a', 1); // Giltigt
function y(a: string): void; // Ogiltigt, inte kompatibelt med implementationssignaturen
function 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).

// Supertyp
class X {
a: string;
constructor(value: string) {
this.a = value;
}
}
// Undertyp
class Y extends X {}
// Undertyp
class Z extends X {}
type GetA = (x: X) => string;
const getA: GetA = x => x.a;
// Bivarians accepterar supertyper
console.log(getA(new X('x'))); // Giltigt
console.log(getA(new Y('Y'))); // Giltigt
console.log(getA(new Z('z'))); // Giltigt

Enums ä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; // Giltigt
const ya: Y = 0; // Giltigt
X.A === Y.A; // Ogiltigt

Instanser 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'); // Ogiltigt

Jä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; // Giltigt
x === z; // Giltigt även om z tillhör en annan arvshierarki

Generics 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 strukturen
interface X<T> {}
const x: X<number> = 1;
const y: X<string> = 'a';
x === y; // Giltigt eftersom typargumentet inte används i den slutliga strukturen

Nä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; // Giltigt

Kom 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 any
let e2: any = e; // Giltigt
let 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 any
g = g1; // Giltigt

Observera 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ängdtermTypeScriptAnteckningar
Tom mängdnever”never” innehåller ingenting förutom sig själv
Enelement-mängdundefined / null / literal type
Ändlig mängdboolean / union
Oändlig mängdstring / number / object
Universell mängdany / unknownVarje 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:

TypeScriptMängdtermExempel
never∅ (tom mängd)const x: never = ‘x’; // Error: Type ‘string’ is not assignable to type ‘never’
Literal typeEnelement-mängdtype X = ‘X’;
type Y = 7;
Värde tilldelbart till TVärde ∈ T (medlem av)type XY = ‘X’ | ‘Y’;
const x: XY = ‘X’;
T1 tilldelbart till T2T1 ⊆ 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 T2T1 ⊆ T2 (delmängd av)type X = ‘X’ extends string ? true : false;
T1 | T2T1 ∪ T2 (union)type XY = ‘X’ | ‘Y’;
type JK = 1 | 2;
T1 & T2T1 ∩ T2 (snitt)type X = { a: string }
type Y = { b: string }
type XY = X & Y
const x: XY = { a: ‘a’, b: ‘b’ }
unknownUniversell mängdconst 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' }; // Giltigt

En 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' }; // Ogiltigt
const j: XY = { a: 'a', b: 'b' }; // Giltigt

Nyckelordet 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; // Giltigt

Tilldela 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;
};
// Typdeklaration
const 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:

Terminal window
npm install --save-dev @types/library-name

Fö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 typning
const w: X = { a: 'a', b: 'b' }; // Ogiltigt på grund av kontrollen av överflödiga egenskaper

Svaga 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); // Giltigt

Eller 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' }); // Giltigt

Strikt 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 tilldelning
var 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ärskhetskontroll

Typinferens

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 string

TypeScript-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 MouseEvent

Typbreddning

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 typ
let y: 'y' | 'x' = 'y'; // Typen för y är en union av literaltyper
y = 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 typ
let 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; // Giltigt

Vi 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; // Giltigt
v.x = 100; // Ogiltigt

Typavsmalnande

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 typen

Switch-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';
}
}