I believe I am trying to convert a wider type to a more narrow type, which is the opposite to the Liskov substitution principle(?)
How can I tell the TS compiler that the narrowed properties "a"
and "b"
exist on the property without using type assertions?
type Original = Record<string, [string, ...string[]]>;
type Converted = Record<"a" | "b", string>;
let orig: Original = { a: ["1"], b: ["2"] };
let conv: Converted = Object.fromEntries(
Object.entries(orig).map(([key, value]) => [key, value[0]]),
);
Type '{ [k: string]: string; }' is missing the following properties from type 'Record<"a" | "b", string>': a, b ts(2739)
I think the satisfies
keyword might be needed here, but I cannot work out how to apply it to the code above.
I believe I am trying to convert a wider type to a more narrow type, which is the opposite to the Liskov substitution principle(?)
How can I tell the TS compiler that the narrowed properties "a"
and "b"
exist on the property without using type assertions?
type Original = Record<string, [string, ...string[]]>;
type Converted = Record<"a" | "b", string>;
let orig: Original = { a: ["1"], b: ["2"] };
let conv: Converted = Object.fromEntries(
Object.entries(orig).map(([key, value]) => [key, value[0]]),
);
Type '{ [k: string]: string; }' is missing the following properties from type 'Record<"a" | "b", string>': a, b ts(2739)
I think the satisfies
keyword might be needed here, but I cannot work out how to apply it to the code above.
It's not easy problem even to approach (how to make it right, what way?). So
as const satisfies
to preserve exact literal types to the end.so a solution (no implementation though) could look like that (seems could be improved though, every improvement seems like a separate question, so the problem is rather big).
You could try to override the standard Object.entries
, etc for example, though I'm not sure how that'd affect existing code.
Playground
type Original = Record<string, [string, ...string[]]>;
type Converted = Record<"a" | "b", string>;
let orig = { a: ["1"], b: ["2"] } as const satisfies Original;
type FromEntries<T extends readonly [string, any]> = {
[K in T[0]]: Extract<T, readonly [K, any]>[1];
};
type Simplify<T> = T extends Function ? T : {[K in keyof T]: Simplify<T[K]>} extends infer A ? A : never;
declare function entries<T extends Record<string, any>>(obj: T): {[I in keyof T]:[I, T[I]] }[keyof T][];
declare function fromEntries<const T extends (readonly [string, any])[]>(obj: T): Simplify<FromEntries<T[number]>>;
declare function map<T extends any, R>(item: T[], cb: (item: T) => R): R[]
const e = entries(orig);
const s = map(e, (item) => {
const [k, v] = item;
return k ==='a' ? [k,v[0]] as const:[k,v[0]] as const;
});
let conv = fromEntries(s) satisfies Converted;
conv
variable doesn't match theConverted
type though does it? You map over theentries
to create an array of objects, not a new singular object --- e.g. the value you're putting intoconv
is[{a: "1"}, {b: "2"}]
, whereas the type would imply a value of{a: "1", b: "2"}
which it's not getting – toastrackengima Commented Mar 7 at 11:58orig
asOriginal
so TS can't possibly know abouta
andb
. So your desired narrowing is indeed the opposite of the LSP, which means it's inherently unsafe (you could changea
toz
and TS couldn't tell that this happened). If you want TS to let you do an unsafe thing, then the most straightforward and reasonable thing to do is use a type assertion. Please edit to clarify why you want/expect to do this without type assertions, because right now this sounds like "I'd like to open a can without using a can opener" and leads to more questions. – jcalz Commented Mar 7 at 13:04