#``[Property in keyof Type as `get${Capitalize<string & Property>}`]: () => Type[Property]``

50 messages · Page 1 of 1 (latest)

worthy grove
#

In https://www.typescriptlang.org/docs/handbook/2/mapped-types.html#key-remapping-via-as, there is this example:

type Getters<Type> = {
    [Property in keyof Type as `get${Capitalize<string & Property>}`]: () => Type[Property]
};
 
interface Person {
    name: string;
    age: number;
    location: string;
}
 
type LazyPerson = Getters<Person>;

which makes LazyPerson { getName: () => string; getAge: () => number; getLocation: () => string; }.
I don't understand why is string & Property needed instead of just Property. AFAIK, keyof returns an union of PropertyKeys, and Capitalize requires its type parameter to be a subtype of string. So does string & Property somehow eliminate property keys that are not strings like number or symbol?

clear schooner
#

So does string & Property somehow eliminate property keys that are not strings like number or symbol?

#

yes, that's exactly right

#

not sure what part confused you, but & means intersection. A & B represents values that are assignable to both A and B. since some symbol property won't be assignable to string, string & Property eliminates it

#

here's a small example:

vital tigerBOT
#
type Example = 1 | 2 | 'a' | 'b'

type StringsOnly = Example & string
//   ^? - type StringsOnly = "a" | "b"
worthy grove
#

But if I change Person to

interface Person {
    [index: string]: number;
}

then what would LazyPerson become?

#

The playground gives this which I don't really understand

clear schooner
#

Capitalize is a weird/special intrinsic so i'm not 100% sure if Capitalize<string> means anything more than just string (my guess is no), but that's basically "an object type where all properties that start with the substring 'get' have a value that is () => number"

#

do you know about index signatures?

worthy grove
#

yes

clear schooner
#

how about template literal types?

worthy grove
#

yes

worthy grove
clear schooner
#

i'm not sure exactly what you mean by "returns" there, but i think the answer is "no"

#

property values are functions

#

() => number, to be exact

#

a value like { getX: () => 42 } is assignable to that type, if that helps

worthy grove
#

What does the key [x: `get${Capitalize<string>}`] mean?

#

it's not even named index

clear schooner
#

the name is irrelevant to the type system. it's only for documentation purposes. similar to parameter names

clear schooner
#

so let's break it apart:

  • Capitalize<string> is "any string that starts with a capital letter"
  • `get${Capitalize<string>}` is a template literal type for a string that starts with the substring 'get' and is followed by Capitalize<string>
  • [x: `get${Capitalize<string>}`] is the index part of an index signature, specifying all properties that start with the substring 'get' followed by Capitalize<string>
worthy grove
#

In this case index takes a number, and LazyPerson has nothing. Can I understand this as when a mapped type iterate through the keys, if some key is malformed, in this case because of Capitalize<number & string>, then that key is just not included in the result?

clear schooner
#

it's not that they are malformed, it's that you're eliminating them entirely

#

if you didn't include the & string you'd see an error (that's what i would call "malformed", maybe)

#

it ends up doing Capitalize<never>, since number & string is never

#

and then it's `get${never}`, which is also just never

#

and doing key remapping (the as) to never is a way to omit the key

#

so you end up with no keys in the output

#

in general when trying to understand a complex type it's a good idea to break it down into parts and understand the individual pieces. usually you can see how they fit back together pretty easily once you get the parts

worthy grove
#

Is never the subtype of all types? Because it can be the type parameter of Capitalize

clear schooner
#

yes, kinda. in type theory terms it is a "bottom type". specifically never is a type that has no values assignable to it, an empty set (and just like an empty set is a subset of all other sets, never is a subtype of all other types)

#

i sometimes think of it as a union type with no members

#

that's why functions that just throw exceptions can be annotated to return never. they "never" return a value

vital tigerBOT
#
const f = (): never => { throw new Error() }
const a = f()
//    ^? - const a: never
clear schooner
#

a will never have a value in that program (execution doesn't even get as far as assigning it a value)

worthy grove
#

and that's why if I index it with "geta" it's an error

clear schooner
#

yes. FWIW this whole thing is overcomplicated if you are only using types with index signatures. Getters seems more useful for object types with specific properties

#

(also FWIW i almost always avoid index signatures in my own code as they're not very safe/sound, but that may not be possible for you)

worthy grove
#

why?

worthy grove
clear schooner
vital tigerBOT
#
type Evil = { [x: string]: string }

const x: Evil = { something: '' }

x.someting.toUpperCase() // this always throws an exception (because it's typo'd)
clear schooner
#

i use typescript instead of javascript to help catch errors like that at compile time. but index signatures allow whole classes of bugs to slip through by design

worthy grove
#

Is x.someting undefined?

clear schooner
#

yes, at runtime it will be

clear schooner
worthy grove
#

How did I miss it lol. Thank you