diff --git a/anon/package.json b/anon/package.json index 1356d98..f807d9c 100644 --- a/anon/package.json +++ b/anon/package.json @@ -1,6 +1,6 @@ { "name": "@pkmn/anon", - "version": "0.1.13", + "version": "0.1.14", "description": "Logic for anonymizing Pokémon Showdown battle simulator logs", "repository": "github:pkmn/stats", "license": "MIT", @@ -16,11 +16,11 @@ "access": "public" }, "dependencies": { - "@pkmn/data": "^0.8.7", + "@pkmn/data": "^0.8.9", "@pkmn/protocol": "^0.6.19" }, "devDependencies": { - "@pkmn/dex": "^0.8.7" + "@pkmn/dex": "^0.8.9" }, "scripts": { "lint": "eslint --cache src --ext ts", diff --git a/anon/src/index.ts b/anon/src/index.ts index 49a0e1d..27c0e87 100644 --- a/anon/src/index.ts +++ b/anon/src/index.ts @@ -55,7 +55,7 @@ export interface Rating { rprd: number; } -type Writeable = { -readonly [P in keyof T]: T[P] }; +type Writeable = {-readonly [P in keyof T]: T[P]}; export const Anonymizer = new class { anonymize( @@ -476,7 +476,7 @@ function copyPokemonSet(pokemon: PokemonSet) { // than negatives here. export class Verifier { readonly names: Set = new Set(); - readonly leaks: Array<{ input: string; output: string }> = []; + readonly leaks: Array<{input: string; output: string}> = []; private regex: RegExp | undefined = undefined; diff --git a/package.json b/package.json index ed6654d..534005d 100644 --- a/package.json +++ b/package.json @@ -4,8 +4,8 @@ "license": "MIT", "dependencies": { "@pkmn/anon": "file:anon", - "@pkmn/data": "^0.8.7", - "@pkmn/dex": "^0.8.7", + "@pkmn/data": "^0.8.9", + "@pkmn/dex": "^0.8.9", "@pkmn/logs": "file:logs", "@pkmn/stats": "file:stats", "json-stringify-pretty-compact": "3.0.0", @@ -17,19 +17,19 @@ "@pkmn/sets": "^5.1.2" }, "devDependencies": { - "@babel/preset-env": "^7.23.9", + "@babel/preset-env": "^7.24.0", "@babel/preset-typescript": "^7.23.3", - "@pkmn/eslint-config": "^6.6.0", + "@pkmn/eslint-config": "^7.1.0", "@types/jest": "^29.5.12", - "@types/node": "^20.11.16", - "@typescript-eslint/eslint-plugin": "^6.20.0", - "@typescript-eslint/parser": "^6.20.0", - "eslint": "^8.56.0", + "@types/node": "^20.11.24", + "@typescript-eslint/eslint-plugin": "^7.1.0", + "@typescript-eslint/parser": "^7.1.0", + "eslint": "^8.57.0", "eslint-plugin-import": "^2.29.1", - "eslint-plugin-jest": "^27.6.3", + "eslint-plugin-jest": "^27.9.0", "jest": "^29.7.0", "subpkg": "^4.1.0", - "tsup": "^8.0.1", + "tsup": "^8.0.2", "typescript": "^5.3.3" }, "subPackages": ["anon", "logs", "stats"], diff --git a/stats/package.json b/stats/package.json index 82fe272..07e7c55 100644 --- a/stats/package.json +++ b/stats/package.json @@ -1,6 +1,6 @@ { "name": "@pkmn/stats", - "version": "0.2.15", + "version": "0.2.16", "description": "Logic for processing usage stats from Pokémon Showdown battle simulator logs", "repository": "github:pkmn/stats", "license": "MIT", @@ -16,10 +16,10 @@ "access": "public" }, "dependencies": { - "@pkmn/data": "^0.8.7" + "@pkmn/data": "^0.8.9" }, "devDependencies": { - "@pkmn/dex": "^0.8.7", + "@pkmn/dex": "^0.8.9", "json-stringify-pretty-compact": "3.0.0" }, "scripts": { diff --git a/stats/src/binary.ts b/stats/src/binary.ts index 27896ec..806666d 100644 --- a/stats/src/binary.ts +++ b/stats/src/binary.ts @@ -8,7 +8,7 @@ const enum EndType { TIE = 1, FORFEIT = 2, FORCED_WIN = 3, - FORCED_TIE = 4, + FORCED_TIE = 4 } const LE = (() => { diff --git a/stats/src/classifier.ts b/stats/src/classifier.ts index c79aa4f..5a0eca1 100644 --- a/stats/src/classifier.ts +++ b/stats/src/classifier.ts @@ -29,7 +29,7 @@ export const Classifier = new class { const originalAbility = pokemon.ability; const species = util.getSpecies(gen, pokemon.species, legacy); - let mega: { species: ID; ability: ID } | undefined; + let mega: {species: ID; ability: ID} | undefined; if (util.isMega(species, legacy)) { mega = { species: toID(species.name), diff --git a/stats/src/display.ts b/stats/src/display.ts index fc1d9bf..84e3e07 100644 --- a/stats/src/display.ts +++ b/stats/src/display.ts @@ -9,7 +9,7 @@ const R = (v: number) => util.round(v, 1e4); export interface DisplayStatistics { battles: number; - pokemon: { [name: string]: T }; + pokemon: {[name: string]: T}; metagame?: DisplayMetagameStatistics; } @@ -23,22 +23,22 @@ export interface DisplayUsageStatistics { weight: number | null; viability: [number, number, number, number]; - abilities: { [name: string]: number }; - items: { [name: string]: number }; - stats: { [stats: string]: number }; - moves: { [name: string]: number }; - teammates: { [name: string]: number }; - counters: { [name: string]: [number, number, number] }; + abilities: {[name: string]: number}; + items: {[name: string]: number}; + stats: {[stats: string]: number}; + moves: {[name: string]: number}; + teammates: {[name: string]: number}; + counters: {[name: string]: [number, number, number]}; } export interface LegacyDisplayUsageStatistics extends Omit { - happinesses?: { [happiness: string]: number }; - spreads: { [spreads: string]: number }; + happinesses?: {[happiness: string]: number}; + spreads: {[spreads: string]: number}; } export interface DisplayMetagameStatistics { - tags: { [tag: string]: number }; + tags: {[tag: string]: number}; stalliness: { histogram: Array<[number, number]>; mean: number; @@ -55,7 +55,7 @@ export interface DetailedUsageStatistics { 'team type': ID | null; 'number of battles': number; }; - data: { [name: string]: DetailedMovesetStatistics }; + data: {[name: string]: DetailedMovesetStatistics}; } export interface DetailedMovesetStatistics { @@ -63,21 +63,21 @@ export interface DetailedMovesetStatistics { usage: number; // num GXE, max GXE, 1% GXE, 20% GXE 'Viability Ceiling': [number, number, number, number]; - Abilities: { [ability: string]: number }; - Items: { [item: string]: number }; - Spreads: { [spread: string]: number }; - Happiness?: { [happiness: string]: number }; - Moves: { [move: string]: number }; + Abilities: {[ability: string]: number}; + Items: {[item: string]: number}; + Spreads: {[spread: string]: number}; + Happiness?: {[happiness: string]: number}; + Moves: {[move: string]: number}; // FIXME: this changed 2021-04 from deltas to raw usage - Teammates: { [pokemon: string]: number }; + Teammates: {[pokemon: string]: number}; // n = sum(POKE1_KOED...DOUBLE_SWITCH) // p = POKE1_KOED + POKE1_SWITCHED_OUT / n // d = sqrt((p * (1 - p)) / n) - 'Checks and Counters': { [pokemon: string]: [number, number, number] }; + 'Checks and Counters': {[pokemon: string]: [number, number, number]}; } // Corrections for Pokémon who have had their names changed over time by developers. -const FIX: {[id: string]: string } = { +const FIX: {[id: string]: string} = { mimikyutotembusted: 'mimikyubustedtotem', }; @@ -101,7 +101,7 @@ export const Display = new class { const unique = computeUnique(stats.pokemon); - const pokemon: { [name: string]: DisplayUsageStatistics } = {}; + const pokemon: {[name: string]: DisplayUsageStatistics} = {}; for (const [species, p] of q) { if (species === 'empty') continue; const usage = calcUsage(p.usage, stats.usage, 6); @@ -143,7 +143,7 @@ export const Display = new class { (a, b) => b[1] - a[1] || a[0].localeCompare(b[0]) ); const W = Math.max(1.0, stats.usage.weighted); - const tags: { [id: string]: number } = {}; + const tags: {[id: string]: number} = {}; for (const [tag, weight] of ts) { const r = R(weight / W); if (!r) break; @@ -179,7 +179,7 @@ export const Display = new class { const mr = metagameReport ? parseMetagameReport(metagameReport) : undefined; const lr = leadsReport ? parseLeadsReport(leadsReport) : undefined; - const pokemon: { [name: string]: LegacyDisplayUsageStatistics } = {}; + const pokemon: {[name: string]: LegacyDisplayUsageStatistics} = {}; for (const [species, {weight: w, outcomes}] of Object.entries(pmr)) { if (species === 'empty') continue; const p = dr.data[species]; @@ -205,7 +205,7 @@ export const Display = new class { lead.real = lead.raw; } - const scored: { [name: string]: {score: number; val: [number, number, number]} } = {}; + const scored: {[name: string]: {score: number; val: [number, number, number]}} = {}; for (const [k, [n]] of Object.entries(p['Checks and Counters'])) { if (!outcomes[k]) continue; const {koedn, switchedn} = outcomes[k]; @@ -253,7 +253,7 @@ export const Display = new class { let metagame: DisplayMetagameStatistics | undefined = undefined; if (mr) { - const tags: { [tag: string]: number } = {}; + const tags: {[tag: string]: number} = {}; for (const tag in mr.tags) { const r = R(mr.tags[tag]); if (!r) break; @@ -292,11 +292,11 @@ function formatEncounterStatistics(v: util.EncounterStatistics) { } function toDisplayObject( - map: { [k: string /* number|ID */]: number }, + map: {[k: string /* number|ID */]: number}, weight: number, display?: (id: string) => string ) { - const obj: { [key: string]: number } = {}; + const obj: {[key: string]: number} = {}; const d = (k: number | string) => (typeof k === 'string' && display ? display(k) : k.toString()); const sorted = Object.entries(map).sort((a, b) => b[1] - a[1] || d(a[0]).localeCompare(d(b[0]))); for (const [k, v] of sorted) { @@ -310,13 +310,13 @@ function toDisplayObject( function getTeammates( gen: Generation, // format: ID, - teammates: { [id: string /* ID */]: number }, + teammates: {[id: string /* ID */]: number}, weight: number, // total: number, stats: Statistics -): { [key: string]: number } { +): {[key: string]: number} { // const real = ['challengecup1v1', '1v1'].includes(format); - const m: { [species: string]: number } = {}; + const m: {[species: string]: number} = {}; for (const [id, w] of Object.entries(teammates)) { const species = gen.species.get(id)?.name; if (!species) continue; @@ -332,9 +332,9 @@ function getTeammates( return toDisplayObject(m, weight); } -function computeUnique(stats: { [id: string /* ID */]: UsageStatistics }) { - const pokemon: { [id: string /* ID */]: { usage: Usage; gxes: number[] } } = {}; - const all: { [id: string /* ID */]: UniqueStatistics } = {}; +function computeUnique(stats: {[id: string /* ID */]: UsageStatistics}) { + const pokemon: {[id: string /* ID */]: {usage: Usage; gxes: number[]}} = {}; + const all: {[id: string /* ID */]: UniqueStatistics} = {}; for (const p in stats) { const usage = newUsage(); @@ -374,7 +374,7 @@ interface UsageReportRowData { } function parseUsageReport(report: string) { - const usage: { [id: string]: UsageReportRowData } = {}; + const usage: {[id: string]: UsageReportRowData} = {}; const lines = report.split('\n'); const battles = Number(lines[0].slice(16)); const avg = Number(lines[1].slice(19)); @@ -401,7 +401,7 @@ interface LeadsReportRowData { } function parseLeadsReport(report: string) { - const usage: { [id: string]: LeadsReportRowData } = {}; + const usage: {[id: string]: LeadsReportRowData} = {}; const lines = report.split('\n'); const total = Number(lines[0].slice(13)); @@ -459,7 +459,7 @@ function partialParseMovesetReport(report: string) { } function parseMetagameReport(report: string) { - const tags: { [tag: string]: number } = {}; + const tags: {[tag: string]: number} = {}; const lines = report.split('\n'); let i = 0; diff --git a/stats/src/parser.ts b/stats/src/parser.ts index e98b723..b2407c8 100644 --- a/stats/src/parser.ts +++ b/stats/src/parser.ts @@ -45,7 +45,7 @@ export interface Player { export interface Team { pokemon: Pokemon[]; - classification: { bias: number; stalliness: number; tags: Set }; + classification: {bias: number; stalliness: number; tags: Set}; } export interface Pokemon { @@ -83,7 +83,7 @@ export const Parser = new class { } if (raw.p1 === raw.p2) throw new Error('Player battling themself'); - const idents: { p1: string[]; p2: string[] } = {p1: [], p2: []}; + const idents: {p1: string[]; p2: string[]} = {p1: [], p2: []}; const battle = ({ matchups: [], turns: raw.turns, @@ -129,7 +129,7 @@ export const Parser = new class { switch: {p1: false, p2: false}, }); - const active: { p1?: Slot; p2?: Slot } = {}; + const active: {p1?: Slot; p2?: Slot} = {}; let flags = emptyFlags(); let turnMatchups: Array<[ID, ID, Outcome]> = []; @@ -347,7 +347,7 @@ function identify( name: string, side: 'p1' | 'p2', battle: Battle, - idents: { p1: string[]; p2: string[] }, + idents: {p1: string[]; p2: string[]}, legacy: boolean, ) { const team = battle[side].team.pokemon; diff --git a/stats/src/reports.ts b/stats/src/reports.ts index fb092f3..7c685c6 100644 --- a/stats/src/reports.ts +++ b/stats/src/reports.ts @@ -9,13 +9,13 @@ interface MovesetStatistics { 'Raw count': number; usage: number; 'Viability Ceiling': [number, number, number, number]; - Abilities: { [key: string]: number }; - Items: { [key: string]: number }; - Spreads: { [key: string]: number }; - Happiness: { [key: string]: number }; - Moves: { [key: string]: number }; - Teammates: { [key: string]: number }; - 'Checks and Counters': { [key: string]: util.EncounterStatistics }; + Abilities: {[key: string]: number}; + Items: {[key: string]: number}; + Spreads: {[key: string]: number}; + Happiness: {[key: string]: number}; + Moves: {[key: string]: number}; + Teammates: {[key: string]: number}; + 'Checks and Counters': {[key: string]: util.EncounterStatistics}; } type UsageTier = 'OU' | 'UU' | 'RU' | 'NU' | 'PU'; @@ -381,8 +381,8 @@ export const Reports = new class { ).then(r => [suffix, r])); } - const n: { [suffix: string]: number } = {}; - const u: { [suffix: string]: Map } = {}; + const n: {[suffix: string]: number} = {}; + const u: {[suffix: string]: Map} = {}; let ntot = 0; for (const [suffix, report] of await Promise.all(reports)) { if (report) { @@ -700,11 +700,11 @@ function toMovesetStatistics(gen: Generation, format: ID, stats: Statistics, min // NOTE: https://www.smogon.com/forums/threads/gen-8-smogon-university-usage-statistics-discussion-thread.3657197/post-8841061 function getTeammates( gen: Generation, - teammates: { [id: string /* ID */]: number }, + teammates: {[id: string /* ID */]: number}, stats: Statistics -): { [key: string]: number } { +): {[key: string]: number} { // const real = ['challengecup1v1', '1v1'].includes(format); - const m: { [species: string]: number } = {}; + const m: {[species: string]: number} = {}; for (const [id, w] of Object.entries(teammates)) { const species = util.displaySpecies(gen, id, legacy); const s = stats.pokemon[id]; @@ -719,8 +719,8 @@ function getTeammates( return util.toDisplayObject(m); } -function forDetailed(cc: { [key: string]: util.EncounterStatistics }) { - const obj: { [key: string]: [number, number, number] } = {}; +function forDetailed(cc: {[key: string]: util.EncounterStatistics}) { + const obj: {[key: string]: [number, number, number]} = {}; for (const [k, v] of Object.entries(cc)) { obj[k] = [round(v.n), round(v.p), round(v.d)]; } diff --git a/stats/src/stats.ts b/stats/src/stats.ts index c466841..3a14811 100644 --- a/stats/src/stats.ts +++ b/stats/src/stats.ts @@ -7,7 +7,7 @@ import * as util from './util'; export interface TaggedStatistics { total: WeightedStatistics; - tags: { [id: string /* ID */]: WeightedStatistics }; + tags: {[id: string /* ID */]: WeightedStatistics}; } export interface WeightedStatistics { @@ -16,7 +16,7 @@ export interface WeightedStatistics { export interface Statistics { battles: number; - pokemon: { [id: string /* ID */]: UsageStatistics }; + pokemon: {[id: string /* ID */]: UsageStatistics}; lead: Usage; usage: Usage; win: Usage; @@ -28,25 +28,25 @@ export interface UsageStatistics { usage: Usage; win: Usage; - abilities: { [id: string /* ID */]: number }; - items: { [id: string /* ID */]: number }; - happinesses: { [num: number]: number }; - spreads: { [spread: string]: number }; - stats: { [stats: string]: number }; - moves: { [id: string /* ID */]: number }; + abilities: {[id: string /* ID */]: number}; + items: {[id: string /* ID */]: number}; + happinesses: {[num: number]: number}; + spreads: {[spread: string]: number}; + stats: {[stats: string]: number}; + moves: {[id: string /* ID */]: number}; - raw: { weight: number; count: number }; - saved: { weight: number; count: number }; + raw: {weight: number; count: number}; + saved: {weight: number; count: number}; - encounters: { [id: string /* ID */]: number /* Outcome */[] }; - teammates: { [id: string /* ID */]: number }; - unique: { [id: string /* ID */]: UniqueStatistics }; + encounters: {[id: string /* ID */]: number /* Outcome */[]}; + teammates: {[id: string /* ID */]: number}; + unique: {[id: string /* ID */]: UniqueStatistics}; } export type UniqueStatistics = - | { r: 0 | 1; w: number; g: number } - | { r: 0 | 1; w: number } - | { g: number }; + | {r: 0 | 1; w: number; g: number} + | {r: 0 | 1; w: number} + | {g: number}; export interface Usage { raw: number; @@ -55,7 +55,7 @@ export interface Usage { } export interface MetagameStatistics { - tags: { [id: string /* ID */]: number }; + tags: {[id: string /* ID */]: number}; stalliness: Array<[number, number]>; } @@ -266,13 +266,13 @@ function getWeights(player: Player, cutoffs: number[], legacy: boolean) { } } - return [weights, save] as [Array<{ s: number; m: number }>, boolean]; + return [weights, save] as [Array<{s: number; m: number}>, boolean]; } function updateStats( gen: Generation, player: Player, - weights: { s: number; m: number }, + weights: {s: number; m: number}, gxe: number | undefined, save: boolean, short: boolean, @@ -320,7 +320,7 @@ function updateStats( if (!u) { p.unique[player.name] = {g: gxe}; } else if (!('g' in u)) { - (u as { r: 0 | 1; w: number; g: number }).g = gxe; + (u as {r: 0 | 1; w: number; g: number}).g = gxe; } else if (u.g < gxe) { u.g = gxe; } @@ -368,7 +368,7 @@ function updateStats( if (win) p.win.weighted += weights.s; const u = p.unique[player.name]; - const c = u as { r: 0 | 1; w: number; g: number }; + const c = u as {r: 0 | 1; w: number; g: number}; if (!u) { p.unique[player.name] = {r: real, w: weights.s}; } else if (!('r' in u)) { @@ -446,7 +446,7 @@ function updateTeammates( pokemon: Pokemon[], i: number, a: ID, - ta: { [id: string /* ID */]: number }, + ta: {[id: string /* ID */]: number}, stats: Statistics, weight: number ) { @@ -512,8 +512,8 @@ function updateEncounters(stats: Statistics, matchups: Array<[ID, ID, Outcome]>, function updateLeads( stats: Statistics, battle: Battle, - weights: { p1: number; p2: number }, - tags: { p1: Set; p2: Set }, + weights: {p1: number; p2: number}, + tags: {p1: Set; p2: Set}, tag?: ID ) { const sides: Array<'p1' | 'p2'> = ['p1', 'p2']; @@ -614,7 +614,7 @@ function combineUsage(a: UsageStatistics, b: UsageStatistics | undefined) { export function combineUnique(a: UniqueStatistics, b: UniqueStatistics) { if (!b) return a; - const c = a as { r?: 0 | 1; w?: number; g?: number }; + const c = a as {r?: 0 | 1; w?: number; g?: number}; if ('r' in b) c.r = ((c.r ?? 0) | b.r) as 0 | 1; if ('w' in b) c.w = 'w' in c ? Math.max(c.w!, b.w) : b.w; if ('g' in b) c.g = 'g' in c ? Math.max(c.g!, b.g) : b.g; @@ -631,7 +631,7 @@ function combineMetagame(a: MetagameStatistics, b: MetagameStatistics | undefine return a; } -function combineMap(a: { [key: string]: number }, b: { [key: string]: number } | undefined) { +function combineMap(a: {[key: string]: number}, b: {[key: string]: number} | undefined) { if (!b) return a; for (const k in b) { a[k] = (a[k] || 0) + b[k]; diff --git a/stats/src/test/integration.test.ts b/stats/src/test/integration.test.ts index 05e2b65..826115f 100644 --- a/stats/src/test/integration.test.ts +++ b/stats/src/test/integration.test.ts @@ -2,8 +2,8 @@ import * as integration from './integration'; describe('Integration', () => { test('process', () => { - const actual: { [file: string]: string } = {}; - const expected: { [file: string]: string } = {}; + const actual: {[file: string]: string} = {}; + const expected: {[file: string]: string} = {}; integration.compare(integration.process(), (file: string, a: string, e: string) => { actual[file] = a; expected[file] = e; diff --git a/stats/src/test/integration.ts b/stats/src/test/integration.ts index dd346b8..cde48e7 100644 --- a/stats/src/test/integration.ts +++ b/stats/src/test/integration.ts @@ -26,7 +26,7 @@ type WeightedReports = Map; interface Reports { usage: string; leads: string; - movesets: { basic: string; detailed: string }; + movesets: {basic: string; detailed: string}; metagame: string; display: string; } @@ -90,7 +90,7 @@ export function process() { return {formats, tiers: ''}; } -export function update(reports: { formats: Map; tiers: string }) { +export function update(reports: {formats: Map; tiers: string}) { const dir = path.resolve(TESTDATA, 'reports'); rmrf(dir); fs.mkdirSync(dir); @@ -117,7 +117,7 @@ export function update(reports: { formats: Map; tiers: string } export function compare( - reports: { formats: Map; tiers: string }, + reports: {formats: Map; tiers: string}, cmp: CompareFn ) { const dir = path.resolve(TESTDATA, 'reports'); diff --git a/stats/src/util.ts b/stats/src/util.ts index 1837f83..d31e51b 100644 --- a/stats/src/util.ts +++ b/stats/src/util.ts @@ -18,10 +18,10 @@ export const enum Outcome { POKE2_UTURN_KOED = 9, POKE1_FODDERED = 10, POKE2_FODDERED = 11, - UNKNOWN = 12, + UNKNOWN = 12 } -const ALIASES: Readonly<{ [id: string]: string }> = aliases; +const ALIASES: Readonly<{[id: string]: string}> = aliases; let DEFAULT!: Generation; export function newGenerations(dex: Dex) { @@ -178,11 +178,11 @@ export function displaySpecies(gen: Generation, name: string, legacy: boolean) { } export function toDisplayObject( - map: { [k: string /* number|ID */]: number }, + map: {[k: string /* number|ID */]: number}, display?: (id: string) => string, p = PRECISION ) { - const obj: { [key: string]: number } = {}; + const obj: {[key: string]: number} = {}; const d = (k: number | string) => (typeof k === 'string' && display ? display(k) : k.toString()); const sorted = Object.entries(map).sort((a, b) => b[1] - a[1] || d(a[0]).localeCompare(d(b[0]))); for (const [k, v] of sorted) { @@ -214,7 +214,7 @@ export interface EncounterStatistics { } export function getChecksAndCounters( - encounters: { [id: string /* ID */]: number /* Outcome */[] }, + encounters: {[id: string /* ID */]: number /* Outcome */[]}, display: [(id: string) => string, (es: EncounterStatistics) => T], min = 20 ) { @@ -233,7 +233,7 @@ export function getChecksAndCounters( } const sorted = cc.sort((a, b) => b[1].score - a[1].score || a[0].localeCompare(b[0])); - const obj: { [key: string]: T } = {}; + const obj: {[key: string]: T} = {}; for (const [k, v] of sorted) { obj[display[0](k)] = display[1](v); }