Eesti

Õppige TypeScripti üleliigsete omaduste kontrolle, et ennetada käitusaegseid vigu ja parandada objekti tüübiohutust robustsete ja prognoositavate JavaScripti rakenduste jaoks.

TypeScripti üleliigsete omaduste kontroll: objekti tüübiohutuse tugevdamine

Tänapäevase tarkvaraarenduse valdkonnas, eriti JavaScriptiga, on koodi terviklikkuse ja prognoositavuse tagamine ülioluline. Kuigi JavaScript pakub tohutut paindlikkust, võib see mõnikord põhjustada käitusaegseid vigu ootamatute andmestruktuuride või omaduste mittevastavuse tõttu. Siin tulebki mängu TypeScript, pakkudes staatilise tüübikontrolli võimekust, mis püüab kinni paljud levinud vead enne, kui need tootmiskeskkonnas avalduvad. Üks TypeScripti võimsamaid, kuid mõnikord valesti mõistetud funktsioone on selle üleliigsete omaduste kontroll.

See postitus süveneb TypeScripti üleliigsete omaduste kontrolli, selgitades, mis need on, miks need on objekti tüübiohutuse seisukohalt üliolulised ja kuidas neid tõhusalt ära kasutada robustsemate ja prognoositavamate rakenduste loomiseks. Uurime erinevaid stsenaariume, levinud lõkse ja parimaid praktikaid, et aidata arendajatel üle maailma, sõltumata nende taustast, seda olulist TypeScripti mehhanismi rakendada.

Põhimõiste mõistmine: mis on üleliigsete omaduste kontroll?

Oma olemuselt on TypeScripti üleliigsete omaduste kontroll kompilaatori mehhanism, mis takistab teil objektiliteraali määramist muutujale, mille tüüp neid lisaomadusi selgesõnaliselt ei luba. Lihtsamalt öeldes, kui defineerite objektiliteraali ja proovite selle määrata kindla tüübimääratlusega (näiteks liidese või tüübivarjunimega) muutujale ning see literaal sisaldab omadusi, mida määratletud tüübis deklareeritud ei ole, märgib TypeScript selle kompileerimise ajal veaks.

Illustreerime seda lihtsa näitega:


interface User {
  name: string;
  age: number;
}

const newUser: User = {
  name: 'Alice',
  age: 30,
  email: 'alice@example.com' // Viga: objektiliteraal võib määrata ainult teadaolevaid omadusi ja 'email' ei eksisteeri tüübis 'User'.
};

Selles koodilõigus defineerime interface'i nimega User, millel on kaks omadust: name ja age. Kui proovime luua objektiliteraali lisaomadusega email ja määrata see muutujale, mille tüübiks on User, tuvastab TypeScript kohe mittevastavuse. Omadus email on 'üleliigne' omadus, kuna see pole User liideses defineeritud. See kontroll tehakse spetsiifiliselt siis, kui kasutate määramiseks objektiliteraali.

Miks on üleliigsete omaduste kontroll oluline?

Üleliigsete omaduste kontrolli tähtsus seisneb nende võimes jõustada lepingut teie andmete ja nende oodatava struktuuri vahel. Nad aitavad kaasa objekti tüübiohutusele mitmel kriitilisel viisil:

Millal üleliigsete omaduste kontroll kehtib?

On ülioluline mõista spetsiifilisi tingimusi, mille korral TypeScript neid kontrolle teostab. Neid rakendatakse peamiselt objektiliteraalidele, kui need määratakse muutujale või edastatakse argumendina funktsioonile.

Stsenaarium 1: objektiliteraalide määramine muutujatele

Nagu ülaltoodud User näites näha, käivitab lisaomadustega objektiliteraali otsene määramine tüübitud muutujale kontrolli.

Stsenaarium 2: objektiliteraalide edastamine funktsioonidele

Kui funktsioon ootab kindlat tüüpi argumenti ja te edastate objektiliteraali, mis sisaldab üleliigseid omadusi, märgib TypeScript selle veaks.


interface Product {
  id: number;
  name: string;
}

function displayProduct(product: Product): void {
  console.log(`Product ID: ${product.id}, Name: ${product.name}`);
}

displayProduct({
  id: 101,
  name: 'Laptop',
  price: 1200 // Viga: tüübi '{ id: number; name: string; price: number; }' argument ei ole määratav parameetrile tüübiga 'Product'.
             // Objektiliteraal võib määrata ainult teadaolevaid omadusi ja 'price' ei eksisteeri tüübis 'Product'.
});

Siin on price omadus funktsioonile displayProduct edastatud objektiliteraalis üleliigne omadus, kuna Product liides seda ei defineeri.

Millal üleliigsete omaduste kontroll *ei* kehti?

Sama oluline on mõista, millal nendest kontrollidest mööda minnakse, et vältida segadust ja teada, millal võib vaja minna alternatiivseid strateegiaid.

1. Kui määramiseks ei kasutata objektiliteraale

Kui määrate objekti, mis ei ole objektiliteraal (nt muutuja, mis juba hoiab objekti), minnakse üleliigsete omaduste kontrollist tavaliselt mööda.


interface Config {
  timeout: number;
}

function setupConfig(config: Config) {
  console.log(`Timeout set to: ${config.timeout}`);
}

const userProvidedConfig = {
  timeout: 5000,
  retries: 3 // See 'retries' omadus on 'Config' järgi üleliigne omadus
};

setupConfig(userProvidedConfig); // Viga puudub!

// Kuigi userProvidedConfig'il on lisaomadus, jäetakse kontroll vahele
// kuna seda ei edastata otse objektiliteraalina.
// TypeScript kontrollib userProvidedConfig'i enda tüüpi.
// Kui userProvidedConfig oleks deklareeritud tüübiga Config, oleks viga tekkinud varem.
// Kuid kui see on deklareeritud kui 'any' või laiem tüüp, lükatakse viga edasi.

// Täpsem viis möödaminemise näitamiseks:
let anotherConfig;

if (Math.random() > 0.5) {
  anotherConfig = {
    timeout: 1000,
    host: 'localhost' // Üleliigne omadus
  };
} else {
  anotherConfig = {
    timeout: 2000,
    port: 8080 // Üleliigne omadus
  };
}

setupConfig(anotherConfig as Config); // Viga puudub tüübikinnituse ja möödaminemise tõttu

// Võti on selles, et 'anotherConfig' ei ole setupConfig'ile määramise hetkel objektiliteraal.
// Kui meil oleks vahemuutuja tüübiga 'Config', ebaõnnestuks esialgne määramine.

// Vahemuutuja näide:
let intermediateConfig: Config;

intermediateConfig = {
  timeout: 3000,
  logging: true // Viga: objektiliteraal võib määrata ainult teadaolevaid omadusi ja 'logging' ei eksisteeri tüübis 'Config'.
};

Esimeses setupConfig(userProvidedConfig) näites on userProvidedConfig muutuja, mis hoiab objekti. TypeScript kontrollib, kas userProvidedConfig tervikuna vastab Config tüübile. See ei rakenda ranget objektiliteraali kontrolli userProvidedConfig'ile endale. Kui userProvidedConfig oleks deklareeritud tüübiga, mis ei vasta Config'ile, tekiks viga selle deklareerimisel või määramisel. Möödapääs toimub, kuna objekt on juba loodud ja muutujale määratud enne funktsioonile edastamist.

2. Tüübikinnitused (Type Assertions)

Üleliigsete omaduste kontrollist saab mööda minna tüübikinnituste abil, kuigi seda tuleks teha ettevaatlikult, kuna see tühistab TypeScripti ohutusgarantiid.


interface Settings {
  theme: 'dark' | 'light';
}

const mySettings = {
  theme: 'dark',
  fontSize: 14 // Üleliigne omadus
} as Settings;

// Siin viga ei teki tüübikinnituse tõttu.
// Me ütleme TypeScriptile: "Usu mind, see objekt vastab Settings tüübile."
console.log(mySettings.theme);
// console.log(mySettings.fontSize); // See põhjustaks käitusaegse vea, kui fontSize tegelikult olemas ei oleks.

3. Indekssignatuuride või laotamissüntaksi kasutamine tüübimääratlustes

Kui teie liides või tüübivarjunimi lubab selgesõnaliselt suvalisi omadusi, siis üleliigsete omaduste kontrolli ei rakendata.

Indekssignatuuride kasutamine:


interface FlexibleObject {
  id: number;
  [key: string]: any; // Lubab mis tahes string-võtit mis tahes väärtusega
}

const flexibleItem: FlexibleObject = {
  id: 1,
  name: 'Widget',
  version: '1.0.0'
};

// Viga puudub, kuna 'name' ja 'version' on indekssignatuuriga lubatud.
console.log(flexibleItem.name);

Laotamissüntaksi kasutamine tüübimääratlustes (vähem levinud kontrollidest otse möödumiseks, rohkem ühilduvate tüüpide määratlemiseks):

Kuigi see ei ole otsene möödapääs, võimaldab laotamine luua uusi objekte, mis sisaldavad olemasolevaid omadusi, ja kontroll kehtib uuele loodud literaalile.

4. `Object.assign()` või laotamissüntaksi kasutamine ühendamiseks

Kui kasutate objektide ühendamiseks Object.assign() või laotamissüntaksit (...), käitub üleliigsete omaduste kontroll erinevalt. See kehtib tulemuseks olevale objektiliteraalile.


interface BaseConfig {
  host: string;
}

interface ExtendedConfig extends BaseConfig {
  port: number;
}

const defaultConfig: BaseConfig = {
  host: 'localhost'
};

const userConfig = {
  port: 8080,
  timeout: 5000 // Üleliigne omadus BaseConfig'i suhtes, kuid oodatud ühendatud tüübi poolt
};

// Laotamine uude objektiliteraali, mis vastab ExtendedConfig'ile
const finalConfig: ExtendedConfig = {
  ...defaultConfig,
  ...userConfig
};

// See on üldiselt korras, kuna 'finalConfig' on deklareeritud kui 'ExtendedConfig'
// ja omadused klapivad. Kontroll tehakse 'finalConfig'i tüübile.

// Vaatleme stsenaariumi, kus see *ebaõnnestuks*:

interface SmallConfig {
  key: string;
}

const data1 = { key: 'abc', value: 123 }; // 'value' on siin üleliigne
const data2 = { key: 'xyz', status: 'active' }; // 'status' on siin üleliigne

// Katse määrata tüübile, mis ei mahuta lisasid

// const combined: SmallConfig = {
//   ...data1, // Viga: objektiliteraal võib määrata ainult teadaolevaid omadusi ja 'value' ei eksisteeri tüübis 'SmallConfig'.
//   ...data2  // Viga: objektiliteraal võib määrata ainult teadaolevaid omadusi ja 'status' ei eksisteeri tüübis 'SmallConfig'.
// };

// Viga tekib, kuna laotamissüntaksi abil moodustatud objektiliteraal
// sisaldab omadusi ('value', 'status'), mida 'SmallConfig'is pole.

// Kui loome laiema tüübiga vahemuutuja:

const temp: any = {
  ...data1,
  ...data2
};

// Seejärel SmallConfig'ile määrates minnakse üleliigsete omaduste kontrollist mööda esialgse literaali loomisel,
// kuid määramisel võib tüübikontroll siiski toimuda, kui temp'i tüüp tuletatakse rangemalt.
// Kuid kui temp on 'any', ei toimu kontrolli enne määramist 'combined'-ile.

// Täpsustame arusaama laotamisest ja üleliigsete omaduste kontrollist:
// Kontroll toimub siis, kui laotamissüntaksi abil loodud objektiliteraal määratakse
// muutujale või edastatakse funktsioonile, mis ootab spetsiifilisemat tüüpi.

interface SpecificShape { 
  id: number;
}

const objA = { id: 1, extra1: 'hello' };
const objB = { id: 2, extra2: 'world' };

// See ebaõnnestub, kui SpecificShape ei luba 'extra1' või 'extra2':
// const merged: SpecificShape = {
//   ...objA,
//   ...objB
// };

// Ebaõnnestumise põhjus on see, et laotamissüntaks loob sisuliselt uue objektiliteraali.
// Kui objA ja objB-l oleksid kattuvad võtmed, jääks hilisem peale. Kompilaator
// näeb seda tulemuseks olevat literaali ja kontrollib seda 'SpecificShape'i vastu.

// Et see tööle saada, võib vaja minna vaheetappi või lubavamat tüüpi:

const tempObj = {
  ...objA,
  ...objB
};

// Nüüd, kui tempObj-l on omadusi, mida SpecificShape'is pole, ebaõnnestub määramine:
// const mergedCorrected: SpecificShape = tempObj; // Viga: objektiliteraal võib määrata ainult teadaolevaid omadusi...

// Võti on selles, et kompilaator analüüsib moodustatava objektiliteraali kuju.
// Kui see literaal sisaldab omadusi, mida sihttüübis pole defineeritud, on tegemist veaga.

// Tüüpiline kasutusjuht laotamissüntaksile üleliigsete omaduste kontrolliga:

interface UserProfile {
  userId: string;
  username: string;
}

interface AdminProfile extends UserProfile {
  adminLevel: number;
}

const baseUserData: UserProfile = {
  userId: 'user-123',
  username: 'coder'
};

const adminData = {
  adminLevel: 5,
  lastLogin: '2023-10-27'
};

// Siin on üleliigsete omaduste kontroll asjakohane:
// const adminProfile: AdminProfile = {
//   ...baseUserData,
//   ...adminData // Viga: objektiliteraal võib määrata ainult teadaolevaid omadusi ja 'lastLogin' ei eksisteeri tüübis 'AdminProfile'.
// };

// Laotamise teel loodud objektiliteraalil on 'lastLogin', mida 'AdminProfile'is pole.
// Selle parandamiseks peaks 'adminData' ideaalis vastama AdminProfile'ile või üleliigset omadust tuleks käsitleda.

// Parandatud lähenemine:
const validAdminData = {
  adminLevel: 5
};

const adminProfileCorrect: AdminProfile = {
  ...baseUserData,
  ...validAdminData
};

console.log(adminProfileCorrect.userId);
console.log(adminProfileCorrect.adminLevel);

Üleliigsete omaduste kontroll kehtib laotamissüntaksi abil loodud tulemuseks olevale objektiliteraalile. Kui see tulemuseks olev literaal sisaldab omadusi, mida sihttüübis deklareeritud ei ole, annab TypeScript veateate.

Strateegiad üleliigsete omaduste käsitlemiseks

Kuigi üleliigsete omaduste kontrollid on kasulikud, on ka legitiimseid stsenaariume, kus teil võib olla lisaomadusi, mida soovite kaasata või erinevalt töödelda. Siin on levinud strateegiad:

1. Jääkomadused (Rest Properties) tüübivarjunimede või liidestega

Saate kasutada jääkparameetri süntaksit (...rest) tüübivarjunimede või liideste sees, et püüda kinni kõik ülejäänud omadused, mis pole selgesõnaliselt defineeritud. See on puhas viis nende üleliigsete omaduste tunnustamiseks ja kogumiseks.


interface UserProfile {
  id: number;
  name: string;
}

interface UserWithMetadata extends UserProfile {
  metadata: {
    [key: string]: any;
  };
}

// Või sagedamini tüübivarjunime ja jääksüntaksiga:
type UserProfileWithMetadata = UserProfile & {
  [key: string]: any;
};

const user1: UserProfileWithMetadata = {
  id: 1,
  name: 'Bob',
  email: 'bob@example.com',
  isAdmin: true
};

// Viga puudub, kuna 'email' ja 'isAdmin' on UserProfileWithMetadata indekssignatuuriga kinni püütud.
console.log(user1.email);
console.log(user1.isAdmin);

// Teine viis, kasutades jääkparameetreid tüübimääratluses:
interface ConfigWithRest {
  apiUrl: string;
  timeout?: number;
  // Püüa kõik muud omadused 'extraConfig'-i
  [key: string]: any;
}

const appConfig: ConfigWithRest = {
  apiUrl: 'https://api.example.com',
  timeout: 5000,
  featureFlags: {
    newUI: true,
    betaFeatures: false
  }
};

console.log(appConfig.featureFlags);

[key: string]: any; või sarnaste indekssignatuuride kasutamine on idiomaatiline viis suvaliste lisaomaduste käsitlemiseks.

2. Destruktureerimine jääksüntaksiga

Kui saate objekti ja peate eraldama konkreetsed omadused, säilitades samal ajal ülejäänud, on jääksüntaksiga destruktureerimine hindamatu.


interface Employee {
  employeeId: string;
  department: string;
}

function processEmployeeData(data: Employee & { [key: string]: any }) {
  const { employeeId, department, ...otherDetails } = data;

  console.log(`Employee ID: ${employeeId}`);
  console.log(`Department: ${department}`);
  console.log('Other details:', otherDetails);
  // otherDetails sisaldab kõiki omadusi, mida pole selgesõnaliselt destruktureeritud,
  // nagu 'salary', 'startDate' jne.
}

const employeeInfo = {
  employeeId: 'emp-789',
  department: 'Engineering',
  salary: 90000,
  startDate: '2022-01-15'
};

processEmployeeData(employeeInfo);

// Isegi kui employeeInfo'l oli algselt lisaomadus, minnakse üleliigsete omaduste kontrollist mööda,
// kui funktsiooni signatuur seda aktsepteerib (nt kasutades indekssignatuuri).
// Kui processEmployeeData oleks rangelt tüübitud kui 'Employee' ja employeeInfo'l oleks 'salary',
// tekiks viga, KUI employeeInfo oleks otse edastatud objektiliteraal.
// Aga siin on employeeInfo muutuja ja funktsiooni tüüp käsitleb lisasid.

3. Kõikide omaduste selgesõnaline defineerimine (kui need on teada)

Kui teate potentsiaalseid lisaomadusi, on parim lähenemine nende lisamine oma liidesesse või tüübivarjunimesse. See tagab kõige suurema tüübiohutuse.


interface UserProfile {
  id: number;
  name: string;
  email?: string; // Valikuline e-post
}

const userWithEmail: UserProfile = {
  id: 2,
  name: 'Charlie',
  email: 'charlie@example.com'
};

const userWithoutEmail: UserProfile = {
  id: 3,
  name: 'David'
};

// Kui proovime lisada omadust, mida UserProfile'is pole:
// const userWithExtra: UserProfile = {
//   id: 4,
//   name: 'Eve',
//   phoneNumber: '555-1234'
// }; // Viga: objektiliteraal võib määrata ainult teadaolevaid omadusi ja 'phoneNumber' ei eksisteeri tüübis 'UserProfile'.

4. `as` kasutamine tüübikinnitusteks (ettevaatlikult)

Nagu varem näidatud, võivad tüübikinnitused üleliigsete omaduste kontrolli maha suruda. Kasutage seda säästlikult ja ainult siis, kui olete objekti kujus absoluutselt kindel.


interface ProductConfig {
  id: string;
  version: string;
}

// Kujutage ette, et see pärineb välisest allikast või vähem rangest moodulist
const externalConfig = {
  id: 'prod-abc',
  version: '1.2',
  debugMode: true // Üleliigne omadus
};

// Kui teate, et 'externalConfig'il on alati 'id' ja 'version' ning soovite seda käsitleda kui ProductConfig'i:
const productConfig = externalConfig as ProductConfig;

// See kinnitus möödub üleliigsete omaduste kontrollist `externalConfig`'il endal.
// Kuid kui te edastaksite objektiliteraali otse:

// const productConfigLiteral: ProductConfig = {
//   id: 'prod-xyz',
//   version: '2.0',
//   debugMode: false
// }; // Viga: objektiliteraal võib määrata ainult teadaolevaid omadusi ja 'debugMode' ei eksisteeri tüübis 'ProductConfig'.

5. Tüübivalvurid (Type Guards)

Keerulisemate stsenaariumide puhul võivad tüübivalvurid aidata tüüpe kitsendada ja omadusi tingimuslikult käsitleda.


interface Shape {
  kind: 'circle' | 'square';
}

interface Circle extends Shape {
  kind: 'circle';
  radius: number;
}

interface Square extends Shape {
  kind: 'square';
  sideLength: number;
}

function calculateArea(shape: Shape) {
  if (shape.kind === 'circle') {
    // TypeScript teab, et 'shape' on siin Circle
    console.log(Math.PI * shape.radius ** 2);
  } else if (shape.kind === 'square') {
    // TypeScript teab, et 'shape' on siin Square
    console.log(shape.sideLength ** 2);
  }
}

const circleData = {
  kind: 'circle' as const, // Kasutades 'as const' literaalse tüübi tuletamiseks
  radius: 10,
  color: 'red' // Üleliigne omadus
};

// Kui see edastatakse calculateArea'le, ootab funktsiooni signatuur 'Shape'i.
// Funktsioon ise pääseb korrektselt 'kind'-ile ligi.
// Kui calculateArea ootaks otse 'Circle't ja saaks circleData
// objektiliteraalina, oleks 'color' probleem.

// Illustreerime üleliigsete omaduste kontrolli funktsiooniga, mis ootab spetsiifilist alamtüüpi:

function processCircle(circle: Circle) {
  console.log(`Processing circle with radius: ${circle.radius}`);
}

// processCircle(circleData); // Viga: tüübi '{ kind: "circle"; radius: number; color: string; }' argument ei ole määratav parameetrile tüübiga 'Circle'.
                         // Objektiliteraal võib määrata ainult teadaolevaid omadusi ja 'color' ei eksisteeri tüübis 'Circle'.

// Selle parandamiseks võite destruktureerida või kasutada circleData jaoks lubavamat tüüpi:

const { color, ...circleDataWithoutColor } = circleData;
processCircle(circleDataWithoutColor);

// Või defineerige circleData laiemat tüüpi hõlmama:

const circleDataWithExtras: Circle & { [key: string]: any } = {
  kind: 'circle',
  radius: 15,
  color: 'blue'
};
processCircle(circleDataWithExtras); // Nüüd see töötab.

Levinud lõksud ja kuidas neid vältida

Isegi kogenud arendajad võivad mõnikord üleliigsete omaduste kontrollidest üllatuda. Siin on levinud lõksud:

Globaalsed kaalutlused ja parimad praktikad

Globaalses ja mitmekesises arenduskeskkonnas töötades on järjepidevate tüübiohutuse praktikate järgimine ülioluline:

Kokkuvõte

TypeScripti üleliigsete omaduste kontroll on selle võimekuse nurgakivi, et pakkuda robustset objekti tüübiohutust. Mõistes, millal ja miks need kontrollid toimuvad, saavad arendajad kirjutada prognoositavamat ja vähem vigaderohket koodi.

Arendajatele üle maailma tähendab selle funktsiooni omaksvõtmine vähem üllatusi käitusajal, lihtsamat koostööd ja paremini hooldatavaid koodibaase. Olenemata sellest, kas loote väikest abivahendit või suuremahulist ettevõtterakendust, aitab üleliigsete omaduste kontrolli valdamine kahtlemata tõsta teie JavaScripti projektide kvaliteeti ja usaldusväärsust.

Peamised järeldused:

Neid põhimõtteid teadlikult rakendades saate oluliselt parandada oma TypeScripti koodi ohutust ja hooldatavust, mis viib edukamate tarkvaraarenduse tulemusteni.