Lietuvių

Įvaldykite TypeScript perteklinių savybių patikras, kad išvengtumėte vykdymo laiko klaidų ir padidintumėte objektų tipų saugumą patikimose, nuspėjamose JavaScript programose.

TypeScript perteklinių savybių patikros: objektų tipų saugumo stiprinimas

Šiuolaikinės programinės įrangos kūrimo srityje, ypač su JavaScript, kodo vientisumo ir nuspėjamumo užtikrinimas yra itin svarbus. Nors JavaScript suteikia didžiulį lankstumą, kartais dėl netikėtų duomenų struktūrų ar savybių neatitikimų gali kilti vykdymo laiko klaidų. Būtent čia suspindi TypeScript, suteikdamas statinio tipizavimo galimybes, kurios pagauna daugelį įprastų klaidų dar prieš joms pasireiškiant produkcijoje. Viena galingiausių, tačiau kartais neteisingai suprantamų TypeScript ypatybių yra perteklinių savybių patikra.

Šiame įraše gilinamasi į TypeScript perteklinių savybių patikras, paaiškinama, kas jos yra, kodėl jos yra kritiškai svarbios objektų tipų saugumui ir kaip jas efektyviai panaudoti kuriant patikimesnes ir labiau nuspėjamas programas. Išnagrinėsime įvairius scenarijus, dažniausiai pasitaikančias klaidas ir geriausias praktikas, kad padėtume programuotojams visame pasaulyje, nepriklausomai nuo jų patirties, išnaudoti šį gyvybiškai svarbų TypeScript mechanizmą.

Pagrindinės koncepcijos supratimas: kas yra perteklinių savybių patikros?

Iš esmės, TypeScript perteklinių savybių patikra yra kompiliatoriaus mechanizmas, neleidžiantis priskirti objekto literalo kintamajam, kurio tipas aiškiai neleidžia tų papildomų savybių. Paprasčiau tariant, jei apibrėžiate objekto literalą ir bandote jį priskirti kintamajam su konkrečiu tipo apibrėžimu (pvz., sąsaja ar tipo pseudonimu), o tas literalas turi savybių, kurios nėra deklaruotos apibrėžtame tipe, TypeScript tai pažymės kaip klaidą kompiliavimo metu.

Pailiustruokime tai paprastu pavyzdžiu:


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

const newUser: User = {
  name: 'Alice',
  age: 30,
  email: 'alice@example.com' // Klaida: Objekto literale gali būti nurodytos tik žinomos savybės, o 'email' neegzistuoja 'User' tipe.
};

Šiame fragmente mes apibrėžiame `interface` pavadinimu `User` su dviem savybėmis: `name` ir `age`. Kai bandome sukurti objekto literalą su papildoma savybe `email` ir priskirti jį kintamajam, tipizuotam kaip `User`, TypeScript iš karto aptinka neatitikimą. `email` savybė yra „perteklinė“ savybė, nes ji nėra apibrėžta `User` sąsajoje. Ši patikra atliekama būtent tada, kai priskyrimui naudojate objekto literalą.

Kodėl perteklinių savybių patikros yra svarbios?

Perteklinių savybių patikrų reikšmė slypi jų gebėjime užtikrinti sutartį tarp jūsų duomenų ir jų tikėtinos struktūros. Jos prisideda prie objektų tipų saugumo keliais kritiniais būdais:

Kada taikomos perteklinių savybių patikros?

Labai svarbu suprasti konkrečias sąlygas, kuriomis TypeScript atlieka šias patikras. Jos pirmiausia taikomos objektų literalams, kai jie priskiriami kintamajam arba perduodami kaip argumentas funkcijai.

1 scenarijus: objektų literalų priskyrimas kintamiesiems

Kaip matyti aukščiau pateiktame `User` pavyzdyje, tiesioginis objekto literalo su papildomomis savybėmis priskyrimas tipizuotam kintamajam sukelia patikrą.

2 scenarijus: objektų literalų perdavimas funkcijoms

Kai funkcija tikisi konkretaus tipo argumento, o jūs perduodate objekto literalą, kuriame yra perteklinių savybių, TypeScript tai pažymės kaip klaidą.


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 // Klaida: Argumentas, kurio tipas '{ id: number; name: string; price: number; }' negali būti priskirtas parametrui, kurio tipas 'Product'.
             // Objekto literale gali būti nurodytos tik žinomos savybės, o 'price' neegzistuoja 'Product' tipe.
});

Čia `price` savybė objekto literale, perduotame į `displayProduct`, yra perteklinė savybė, nes `Product` sąsaja jos neapibrėžia.

Kada perteklinių savybių patikros *netaikomos*?

Supratimas, kada šios patikros yra apeinamos, yra lygiai taip pat svarbus, siekiant išvengti painiavos ir žinoti, kada gali prireikti alternatyvių strategijų.

1. Kai priskyrimui nenaudojami objektų literalai

Jei priskiriate objektą, kuris nėra objekto literalas (pvz., kintamąjį, kuriame jau yra objektas), perteklinių savybių patikra paprastai yra apeinama.


interface Config {
  timeout: number;
}

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

const userProvidedConfig = {
  timeout: 5000,
  retries: 3 // Ši 'retries' savybė yra perteklinė pagal 'Config'
};

setupConfig(userProvidedConfig); // Jokios klaidos!

// Nors userProvidedConfig turi papildomą savybę, patikra praleidžiama,
// nes tai nėra tiesiogiai perduodamas objekto literalas.
// TypeScript tikrina paties userProvidedConfig tipą.
// Jei userProvidedConfig būtų deklaruotas su 'Config' tipu, klaida atsirastų anksčiau.
// Tačiau, jei deklaruotas kaip 'any' ar platesnis tipas, klaida atidedama.

// Tikslesnis būdas parodyti apėjimą:
let anotherConfig;

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

setupConfig(anotherConfig as Config); // Jokios klaidos dėl tipo tvirtinimo ir apėjimo

// Svarbiausia, kad 'anotherConfig' nėra objekto literalas priskyrimo į setupConfig metu.
// Jei turėtume tarpinį kintamąjį, tipizuotą kaip 'Config', pradinis priskyrimas nepavyktų.

// Tarpinio kintamojo pavyzdys:
let intermediateConfig: Config;

intermediateConfig = {
  timeout: 3000,
  logging: true // Klaida: Objekto literale gali būti nurodytos tik žinomos savybės, o 'logging' neegzistuoja 'Config' tipe.
};

Pirmajame `setupConfig(userProvidedConfig)` pavyzdyje `userProvidedConfig` yra kintamasis, laikantis objektą. TypeScript tikrina, ar `userProvidedConfig` kaip visuma atitinka `Config` tipą. Ji netaiko griežtos objekto literalo patikros pačiam `userProvidedConfig`. Jei `userProvidedConfig` būtų deklaruotas su tipu, kuris neatitinka `Config`, klaida atsirastų jo deklaravimo ar priskyrimo metu. Apėjimas įvyksta, nes objektas jau yra sukurtas ir priskirtas kintamajam prieš perduodant jį funkcijai.

2. Tipo tvirtinimai

Galite apeiti perteklinių savybių patikras naudodami tipo tvirtinimus, nors tai turėtų būti daroma atsargiai, nes tai paneigia TypeScript saugumo garantijas.


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

const mySettings = {
  theme: 'dark',
  fontSize: 14 // Perteklinė savybė
} as Settings;

// Čia nėra klaidos dėl tipo tvirtinimo.
// Mes sakome TypeScript: „Pasitikėk manimi, šis objektas atitinka Settings.“
console.log(mySettings.theme);
// console.log(mySettings.fontSize); // Tai sukeltų vykdymo laiko klaidą, jei fontSize iš tikrųjų nebūtų.

3. Indeksų signatūrų arba sklaidos sintaksės naudojimas tipo apibrėžimuose

Jei jūsų sąsaja ar tipo pseudonimas aiškiai leidžia savavališkas savybes, perteklinių savybių patikros nebus taikomos.

Indeksų signatūrų naudojimas:


interface FlexibleObject {
  id: number;
  [key: string]: any; // Leidžia bet kokį eilutės raktą su bet kokia reikšme
}

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

// Jokios klaidos, nes 'name' ir 'version' leidžia indekso signatūra.
console.log(flexibleItem.name);

Sklaidos sintaksės naudojimas tipo apibrėžimuose (mažiau paplitęs tiesioginiam patikrų apėjimui, labiau skirtas suderinamų tipų apibrėžimui):

Nors tai nėra tiesioginis apėjimas, sklaida leidžia kurti naujus objektus, kurie apima esamas savybes, o patikra taikoma naujai suformuotam literalui.

4. `Object.assign()` arba sklaidos sintaksės naudojimas sujungimui

Kai naudojate `Object.assign()` arba sklaidos sintaksę (`...`) objektams sujungti, perteklinių savybių patikra elgiasi kitaip. Ji taikoma galutiniam formuojamam objekto literalui.


interface BaseConfig {
  host: string;
}

interface ExtendedConfig extends BaseConfig {
  port: number;
}

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

const userConfig = {
  port: 8080,
  timeout: 5000 // Perteklinė savybė lyginant su BaseConfig, bet tikimasi sujungtame tipe
};

// Sklaida į naują objekto literalą, kuris atitinka ExtendedConfig
const finalConfig: ExtendedConfig = {
  ...defaultConfig,
  ...userConfig
};

// Tai paprastai yra gerai, nes 'finalConfig' yra deklaruotas kaip 'ExtendedConfig'
// ir savybės sutampa. Patikra atliekama pagal 'finalConfig' tipą.

// Apsvarstykime scenarijų, kai tai *nepavyktų*:

interface SmallConfig {
  key: string;
}

const data1 = { key: 'abc', value: 123 }; // 'value' čia yra papildoma
const data2 = { key: 'xyz', status: 'active' }; // 'status' čia yra papildoma

// Bandant priskirti tipui, kuris neleidžia papildomų savybių

// const combined: SmallConfig = {
//   ...data1, // Klaida: Objekto literale gali būti nurodytos tik žinomos savybės, o 'value' neegzistuoja 'SmallConfig' tipe.
//   ...data2  // Klaida: Objekto literale gali būti nurodytos tik žinomos savybės, o 'status' neegzistuoja 'SmallConfig' tipe.
// };

// Klaida atsiranda, nes objekto literale, suformuotame sklaidos sintakse,
// yra savybių ('value', 'status'), kurių nėra 'SmallConfig'.

// Jei sukursime tarpinį kintamąjį su platesniu tipu:

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

// Tada priskiriant SmallConfig, perteklinių savybių patikra yra apeinama pradinio literalo kūrimo metu,
// bet tipo patikra priskyrimo metu vis tiek gali įvykti, jei temp tipas yra griežčiau nustatytas.
// Tačiau, jei temp yra 'any', jokia patikra nevyksta iki priskyrimo 'combined'.

// Patikslinkime supratimą apie sklaidą su perteklinių savybių patikromis:
// Patikra vyksta, kai sklaidos sintakse sukurtas objekto literalas yra priskiriamas
// kintamajam arba perduodamas funkcijai, kuri tikisi konkretesnio tipo.

interface SpecificShape { 
  id: number;
}

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

// Tai nepavyks, jei SpecificShape neleidžia 'extra1' ar 'extra2':
// const merged: SpecificShape = {
//   ...objA,
//   ...objB
// };

// Priežastis, kodėl tai nepavyksta, yra ta, kad sklaidos sintaksė efektyviai sukuria naują objekto literalą.
// Jei objA ir objB turėtų sutampančių raktų, laimėtų vėlesnis. Kompiliatorius
// mato šį gautą literalą ir tikrina jį pagal 'SpecificShape'.

// Kad tai veiktų, gali prireikti tarpinio žingsnio arba labiau leidžiančio tipo:

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

// Dabar, jei tempObj turi savybių, kurių nėra SpecificShape, priskyrimas nepavyks:
// const mergedCorrected: SpecificShape = tempObj; // Klaida: Objekto literale gali būti nurodytos tik žinomos savybės...

// Svarbiausia, kad kompiliatorius analizuoja formuojamo objekto literalo formą.
// Jei tame literale yra savybių, neapibrėžtų tiksliniame tipe, tai yra klaida.

// Tipiškas sklaidos sintaksės naudojimo atvejis su perteklinių savybių patikromis:

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

// Čia yra svarbi perteklinių savybių patikra:
// const adminProfile: AdminProfile = {
//   ...baseUserData,
//   ...adminData // Klaida: Objekto literale gali būti nurodytos tik žinomos savybės, o 'lastLogin' neegzistuoja 'AdminProfile' tipe.
// };

// Objekto literale, sukurtame sklaidos būdu, yra 'lastLogin', kurio nėra 'AdminProfile'.
// Norint tai ištaisyti, 'adminData' idealiai turėtų atitikti AdminProfile arba perteklinė savybė turėtų būti apdorota.

// Pataisytas požiūris:
const validAdminData = {
  adminLevel: 5
};

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

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

Perteklinių savybių patikra taikoma galutiniam objekto literalui, sukurtam sklaidos sintakse. Jei šiame galutiniame literale yra savybių, kurios nėra deklaruotos tiksliniame tipe, TypeScript praneš apie klaidą.

Strategijos perteklinių savybių tvarkymui

Nors perteklinių savybių patikros yra naudingos, yra teisėtų scenarijų, kai galite turėti papildomų savybių, kurias norite įtraukti ar apdoroti kitaip. Štai keletas įprastų strategijų:

1. Liekanos savybės su tipo pseudonimais ar sąsajomis

Galite naudoti liekanos parametrų sintaksę (`...rest`) tipo pseudonimuose ar sąsajose, kad surinktumėte visas likusias savybes, kurios nėra aiškiai apibrėžtos. Tai yra švarus būdas pripažinti ir surinkti šias perteklines savybes.


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

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

// Arba dažniau su tipo pseudonimu ir liekanos sintakse:
type UserProfileWithMetadata = UserProfile & {
  [key: string]: any;
};

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

// Jokios klaidos, nes 'email' ir 'isAdmin' yra surenkamos indekso signatūros UserProfileWithMetadata.
console.log(user1.email);
console.log(user1.isAdmin);

// Kitas būdas naudojant liekanos parametrus tipo apibrėžime:
interface ConfigWithRest {
  apiUrl: string;
  timeout?: number;
  // Sugaudyti visas kitas savybes į 'extraConfig'
  [key: string]: any;
}

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

console.log(appConfig.featureFlags);

Naudojimas `[key: string]: any;` ar panašių indeksų signatūrų yra idiomatinis būdas tvarkyti savavališkas papildomas savybes.

2. Destrukturizavimas su liekanos sintakse

Kai gaunate objektą ir reikia išskirti konkrečias savybes, paliekant likusias, destrukturizavimas su liekanos sintakse yra neįkainojamas.


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 turės visas savybes, kurios nebuvo aiškiai destrukturizuotos,
  // pvz., 'salary', 'startDate' ir t.t.
}

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

processEmployeeData(employeeInfo);

// Net jei employeeInfo iš pradžių turėjo papildomą savybę, perteklinių savybių patikra
// apeinama, jei funkcijos signatūra tai leidžia (pvz., naudojant indekso signatūrą).
// Jei processEmployeeData būtų griežtai tipizuota kaip 'Employee', o employeeInfo turėtų 'salary',
// klaida atsirastų, JEI employeeInfo būtų tiesiogiai perduotas objekto literalas.
// Bet čia employeeInfo yra kintamasis, o funkcijos tipas tvarko papildomas savybes.

3. Aiškus visų savybių apibrėžimas (jei žinomos)

Jei žinote galimas papildomas savybes, geriausias požiūris yra pridėti jas į savo sąsają ar tipo pseudonimą. Tai suteikia didžiausią tipų saugumą.


interface UserProfile {
  id: number;
  name: string;
  email?: string; // Pasirenkamas el. paštas
}

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

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

// Jei bandysime pridėti savybę, kurios nėra UserProfile:
// const userWithExtra: UserProfile = {
//   id: 4,
//   name: 'Eve',
//   phoneNumber: '555-1234'
// }; // Klaida: Objekto literale gali būti nurodytos tik žinomos savybės, o 'phoneNumber' neegzistuoja 'UserProfile' tipe.

4. `as` naudojimas tipo tvirtinimams (atsargiai)

Kaip parodyta anksčiau, tipo tvirtinimai gali nuslopinti perteklinių savybių patikras. Naudokite tai saikingai ir tik tada, kai esate visiškai tikri dėl objekto formos.


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

// Įsivaizduokite, kad tai ateina iš išorinio šaltinio ar mažiau griežto modulio
const externalConfig = {
  id: 'prod-abc',
  version: '1.2',
  debugMode: true // Perteklinė savybė
};

// Jei žinote, kad 'externalConfig' visada turės 'id' ir 'version' ir norite jį traktuoti kaip ProductConfig:
const productConfig = externalConfig as ProductConfig;

// Šis tvirtinimas apeina perteklinių savybių patikrą pačiam `externalConfig`.
// Tačiau, jei perduotumėte objekto literalą tiesiogiai:

// const productConfigLiteral: ProductConfig = {
//   id: 'prod-xyz',
//   version: '2.0',
//   debugMode: false
// }; // Klaida: Objekto literale gali būti nurodytos tik žinomos savybės, o 'debugMode' neegzistuoja 'ProductConfig' tipe.

5. Tipų apsaugos

Sudėtingesniems scenarijams tipų apsaugos (type guards) gali padėti susiaurinti tipus ir sąlygiškai tvarkyti savybes.


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 žino, kad 'shape' čia yra Circle
    console.log(Math.PI * shape.radius ** 2);
  } else if (shape.kind === 'square') {
    // TypeScript žino, kad 'shape' čia yra Square
    console.log(shape.sideLength ** 2);
  }
}

const circleData = {
  kind: 'circle' as const, // Naudojant 'as const' literalo tipo išvedimui
  radius: 10,
  color: 'red' // Perteklinė savybė
};

// Perduodant į calculateArea, funkcijos signatūra tikisi 'Shape'.
// Pati funkcija teisingai pasieks 'kind'.
// Jei calculateArea tikėtųsi 'Circle' tiesiogiai ir gautų circleData
// kaip objekto literalą, 'color' būtų problema.

// Pailiustruokime perteklinių savybių patikrą su funkcija, kuri tikisi konkretaus potipio:

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

// processCircle(circleData); // Klaida: Argumentas, kurio tipas '{ kind: "circle"; radius: number; color: string; }' negali būti priskirtas parametrui, kurio tipas 'Circle'.
                         // Objekto literale gali būti nurodytos tik žinomos savybės, o 'color' neegzistuoja 'Circle' tipe.

// Norint tai ištaisyti, galite destrukturizuoti arba naudoti labiau leidžiantį tipą circleData:

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

// Arba apibrėžti circleData, įtraukiant platesnį tipą:

const circleDataWithExtras: Circle & { [key: string]: any } = {
  kind: 'circle',
  radius: 15,
  color: 'blue'
};
processCircle(circleDataWithExtras); // Dabar veikia.

Dažniausios klaidos ir kaip jų išvengti

Net patyrę programuotojai kartais gali būti nustebinti perteklinių savybių patikrų. Štai dažniausiai pasitaikančios klaidos:

Globalūs aspektai ir geriausios praktikos

Dirbant globalioje, įvairialypėje kūrimo aplinkoje, nuoseklių tipų saugumo praktikų laikymasis yra labai svarbus:

Išvada

TypeScript perteklinių savybių patikros yra vienas iš kertinių akmenų, užtikrinančių patikimą objektų tipų saugumą. Suprasdami, kada ir kodėl šios patikros vyksta, programuotojai gali rašyti labiau nuspėjamą, mažiau klaidų turintį kodą.

Programuotojams visame pasaulyje šios funkcijos įsisavinimas reiškia mažiau netikėtumų vykdymo metu, lengvesnį bendradarbiavimą ir labiau palaikomas kodo bazes. Nesvarbu, ar kuriate mažą įrankį, ar didelio masto verslo programą, perteklinių savybių patikrų įvaldymas neabejotinai pakels jūsų JavaScript projektų kokybę ir patikimumą.

Svarbiausi aspektai:

Sąmoningai taikydami šiuos principus, galite ženkliai pagerinti savo TypeScript kodo saugumą ir palaikomumą, kas veda prie sėkmingesnių programinės įrangos kūrimo rezultatų.