Why keyof Record wont actually give the keys of the record ?

I have this exemple :
type GlobalStuff = {
description : C,
validate : (descriptionKeys : Record<keyof C, string> | undefined) => Boolean
}

type C = Record<string,object>


const test : GlobalStuff = {
description : {
test: {}
},
validate : (descriptionKeys) => {
//no auto completed
descriptionKeys.test
return true
}
}
type GlobalStuff = {
description : C,
validate : (descriptionKeys : Record<keyof C, string> | undefined) => Boolean
}

type C = Record<string,object>


const test : GlobalStuff = {
description : {
test: {}
},
validate : (descriptionKeys) => {
//no auto completed
descriptionKeys.test
return true
}
}
So based on what i describe with type i would have hope that descriptionsKeys would get auto completion base on the description the user give but it doesn't thinkies The type of descriptionKeys is Record<string, string> and not Record<"test" , string> and if i had more keys to desc i would hope to get these keys to in the Record. Any idea of where i m wrong ?
12 Replies
thejessewinton
thejessewinton10mo ago
It's because of type C = Record<string,object>. When you're using Record<string, object>, the key is essentially being read as "any string", and it doesn't know how to evaluate it beyond that, since it doesn't know the shape of the object in your validate function. Someone else might have a more sophisticated solution, but here's an easy way around it:
const objectToHaveKeysFrom = {
test: {},
}

type C = Record<keyof typeof objectToHaveKeysFrom, object>

type GlobalStuff = {
description : C,
validate : (descriptionKeys: Record<keyof C, | string> | undefined) => Boolean
}

const test: GlobalStuff = {
description : {
...objectToHaveKeysFrom
},
validate : (descriptionKeys) => {
//no auto completed
descriptionKeys?.test
return true
}
}
const objectToHaveKeysFrom = {
test: {},
}

type C = Record<keyof typeof objectToHaveKeysFrom, object>

type GlobalStuff = {
description : C,
validate : (descriptionKeys: Record<keyof C, | string> | undefined) => Boolean
}

const test: GlobalStuff = {
description : {
...objectToHaveKeysFrom
},
validate : (descriptionKeys) => {
//no auto completed
descriptionKeys?.test
return true
}
}
You define the object explicitly outside of test, which is that objectToHaveKeysFrom, then you extract those keys into type C, and pass the object into test.
stevensensei
stevensensei10mo ago
It’s a good start i think but than the user is unable to declare other think directly in description 😅 At the end i want to genrate a zod schema and parse it
erik.gh
erik.gh10mo ago
what do you mean with generate a zod schema?
stevensensei
stevensensei10mo ago
The idea is in the description key the user describe an objet not usine zod notation. And based on that i create a dynamic zod schema to check the value before passing it to the function
erik.gh
erik.gh10mo ago
okay so maybe this helps you
type Description = Record<string, object>;

type Validate<TDesc extends Description> = (
descriptionKeys: Record<keyof TDesc, string> | undefined
) => Boolean;

type GlobalStuff = <TDesc extends Description>(
description: TDesc,
validate: Validate<TDesc>
) => { description: TDesc; validate: Validate<TDesc> };

const test: GlobalStuff = (description, validate) => ({
description,
validate,
});

test({test: {}}, (descriptionKeys) => {
descriptionKeys?.test
return true
})
type Description = Record<string, object>;

type Validate<TDesc extends Description> = (
descriptionKeys: Record<keyof TDesc, string> | undefined
) => Boolean;

type GlobalStuff = <TDesc extends Description>(
description: TDesc,
validate: Validate<TDesc>
) => { description: TDesc; validate: Validate<TDesc> };

const test: GlobalStuff = (description, validate) => ({
description,
validate,
});

test({test: {}}, (descriptionKeys) => {
descriptionKeys?.test
return true
})
wrapping the definition in a function allows for inference using generics
stevensensei
stevensensei10mo ago
But then i can’t do the parsing before the validate function
erik.gh
erik.gh10mo ago
where would you do the parsing in your example?
stevensensei
stevensensei10mo ago
interface IAuthOptions{
credentials: TCred
authenticate(credentials: Record<keyof typeof Tcred, string>): Promise<User | null>;
}

export type TCred = Record<string, string>;
class AuthOptions{
public credentials: TCred;
public credentialsSchema: z.ZodObject;
public authenticate(credentials: Record<keyof typeof Tcred, string>): Promise<User | null>;
constructor({authenticate, credentials}: IAuthOptions){
this.credentials = credentials;
this.credentialsSchema = z.object({
/*Generate based on credentials */
});
this.authenticate = authenticate // should be generate by user;
}

public onLogin(credentials: unkown): void {
const parsed = credentialsSchema.parse(credentials);
this.authenticate(parsed);
}
}
interface IAuthOptions{
credentials: TCred
authenticate(credentials: Record<keyof typeof Tcred, string>): Promise<User | null>;
}

export type TCred = Record<string, string>;
class AuthOptions{
public credentials: TCred;
public credentialsSchema: z.ZodObject;
public authenticate(credentials: Record<keyof typeof Tcred, string>): Promise<User | null>;
constructor({authenticate, credentials}: IAuthOptions){
this.credentials = credentials;
this.credentialsSchema = z.object({
/*Generate based on credentials */
});
this.authenticate = authenticate // should be generate by user;
}

public onLogin(credentials: unkown): void {
const parsed = credentialsSchema.parse(credentials);
this.authenticate(parsed);
}
}
The idea is there i want to inject the authenticate function of the user when someone tries to login a bit like nextjs does but before that based on the credentials they declare i wanna parse it with zod. The code exemple in message before were just test for this
erik.gh
erik.gh10mo ago
And what is the relation between this class implementation and the snippet you provided in the question? I don't see the problem with the implementation above
stevensensei
stevensensei10mo ago
I must admit that all snipet are out dated but have the same goal as here i just worked on it and made it a class so you can have more info but there is the same issue of i want authenticate to get the autocomplete of the keys the credentials the user declare.but when you go for creating a class and declare the authenticate function in the constructor you aren't getting the autocomplete
type TCred = Record<string, object>;
interface IAuthOptions{
credentials: TCred
authenticate(credentials: Record<keyof TCred, string>): Promise<User | null>;
}


export type User = {
id: string;
username : string;
}
class AuthOptions implements IAuthOptions{
public credentialsSchema: z.ZodObject<z.ZodRawShape>;
public credentials: TCred;
public authenticate: (credentials: Record<keyof TCred, string>) => Promise<User | null>;
constructor({authenticate, credentials}: IAuthOptions){
this.credentials = credentials;
this.credentialsSchema = z.object({
/*Generate based on credentials */
});
this.authenticate = authenticate;
}

public onLogin(req: Request): void {
const parsed = credentialsSchema.parse(req.body);
this.authenticate(parsed);
}

}

new AuthOptions({
credentials: {
username: {},
password: {}
},
authenticate: async (credentials) => {
/*User code here to do there own auth equivalent to validate in old exemple*/
return null;
}
})
type TCred = Record<string, object>;
interface IAuthOptions{
credentials: TCred
authenticate(credentials: Record<keyof TCred, string>): Promise<User | null>;
}


export type User = {
id: string;
username : string;
}
class AuthOptions implements IAuthOptions{
public credentialsSchema: z.ZodObject<z.ZodRawShape>;
public credentials: TCred;
public authenticate: (credentials: Record<keyof TCred, string>) => Promise<User | null>;
constructor({authenticate, credentials}: IAuthOptions){
this.credentials = credentials;
this.credentialsSchema = z.object({
/*Generate based on credentials */
});
this.authenticate = authenticate;
}

public onLogin(req: Request): void {
const parsed = credentialsSchema.parse(req.body);
this.authenticate(parsed);
}

}

new AuthOptions({
credentials: {
username: {},
password: {}
},
authenticate: async (credentials) => {
/*User code here to do there own auth equivalent to validate in old exemple*/
return null;
}
})
Alan Ibarra-2310
Alan Ibarra-231010mo ago
I wasn't able to get your implementation fully working, I just don't know how to generate the schema based on the credentials. I tried to do it a different way of passing the schema and the credentials together. I'm not sure if this is what your are looking for but here it is
import {z} from "zod"
type TCred = Record<string, object>;
interface IAuthOptions<T extends TCred>{
credentials: T
authenticate(credentials: T): Promise<User | null>;
credentialsSchema: z.ZodObject<{[K in keyof T]: z.ZodObject<{}, "strip", z.ZodTypeAny, {}, {}> }, any, any, T>
}


type R = {
username: z.ZodObject<{}, "strip", z.ZodTypeAny, {}, {}>;
password: z.ZodObject<{}, "strip", z.ZodTypeAny, {}, {}>;
}


export type User = {
id: string;
username : string;
}



class AuthOptions<T extends TCred> implements IAuthOptions<T>{
public credentials: T;
public authenticate: IAuthOptions<T>["authenticate"]
public credentialsSchema: IAuthOptions<T>["credentialsSchema"]
constructor({authenticate, credentials, credentialsSchema}: IAuthOptions<T>){
this.credentials = credentials;
this.authenticate = authenticate;
this.credentialsSchema = credentialsSchema
}

public onLogin(req: Request): void {
const parsed = this.credentialsSchema.parse(req.body);
this.authenticate(parsed);
}

}

new AuthOptions({
credentials: {
username: {id: "hello"},
password: {},
},
credentialsSchema: z.object({
username: z.object({id: z.string()}),
password: z.object({}),
}),
authenticate: async (credentials) => {
/*User code here to do there own auth equivalent to validate in old exemple*/
credentials.username.id
return null;
}
})
import {z} from "zod"
type TCred = Record<string, object>;
interface IAuthOptions<T extends TCred>{
credentials: T
authenticate(credentials: T): Promise<User | null>;
credentialsSchema: z.ZodObject<{[K in keyof T]: z.ZodObject<{}, "strip", z.ZodTypeAny, {}, {}> }, any, any, T>
}


type R = {
username: z.ZodObject<{}, "strip", z.ZodTypeAny, {}, {}>;
password: z.ZodObject<{}, "strip", z.ZodTypeAny, {}, {}>;
}


export type User = {
id: string;
username : string;
}



class AuthOptions<T extends TCred> implements IAuthOptions<T>{
public credentials: T;
public authenticate: IAuthOptions<T>["authenticate"]
public credentialsSchema: IAuthOptions<T>["credentialsSchema"]
constructor({authenticate, credentials, credentialsSchema}: IAuthOptions<T>){
this.credentials = credentials;
this.authenticate = authenticate;
this.credentialsSchema = credentialsSchema
}

public onLogin(req: Request): void {
const parsed = this.credentialsSchema.parse(req.body);
this.authenticate(parsed);
}

}

new AuthOptions({
credentials: {
username: {id: "hello"},
password: {},
},
credentialsSchema: z.object({
username: z.object({id: z.string()}),
password: z.object({}),
}),
authenticate: async (credentials) => {
/*User code here to do there own auth equivalent to validate in old exemple*/
credentials.username.id
return null;
}
})