Let say I have this JavaScript object:
const myObject = {
id: 'my_id', // Base value that following value's structures will be depended on (always pascal case).
upperID: 'MY_ID', // Uppercase of `id`.
camelID: 'myId', // Camel case of `id`.
}
I want to use TypeScript to ensure that id
is always pascal lower case. The upperID
and camelID
has same "value" with different string structure as above. What would be the best way to declare this myObject
type in TypeScript?
The upper case one is pretty easy with the provided Uppercase<T>
utility type.
type CasedIds<ID extends string> = {
id: ID,
upperID: Uppercase<ID>
}
const myObject: CasedIds<'my_id'> = {
id: 'my_id',
upperID: 'MY_ID',
} as const
The camel case one gets tricky. So first you need a type that can do this to a string.
There's probably a few ways to do this. Here's one.
type CamelCase<T extends string> =
T extends `${infer Pre}_${infer Letter}${infer Post}`
? `${Pre}${Capitalize<Letter>}${CamelCase<Post>}`
: T
type TestCamel = CamelCase<'abc_def_ghi'> // 'abcDefGhi'
Let's walk through this.
This generic type takes a string type as the generic parameter T
. It then check to see if T
extends a string of a certain pattern that contains an underscore followed by some characters.
If it does, then infer some substrings from that pattern. Infer the part before the underscore as Pre
, the character after the underscore as Letter
and the rest of the string as Post
. Else, just use the string type as is.
Then we can make a new string from the Pre
the Letter
capitalized, and the Post
. But there maybe more underscores, so we do the whole thing again on Post
. This is a recursive type that stops recursing when there are no underscores left.
Using that the rest is easy:
type CamelCase<T extends string> =
T extends `${infer Pre}_${infer Letter}${infer Post}`
? `${Pre}${Capitalize<Letter>}${CamelCase<Post>}`
: T
type CasedIds<ID extends string> = {
id: ID,
upperID: Uppercase<ID>
camelID: CamelCase<ID>
}
const myObject: CasedIds<'my_id'> = {
id: 'my_id',
upperID: 'MY_ID',
camelID: 'myId',
} as const
Although you'll probably want a function build these for you:
function makeId<T extends string>(id: T): CasedIds<T> {
return {} as unknown as CasedIds<T> // mock a real implementation
}
const myId = makeId('my_id')
myId.id // type: 'my_id'
myId.upperID // type: 'MY_ID'
myId.camelID // type: 'myId'