Can you create types with dynamic literals?

Say I have this function:
const createType = <T extends string>(code: T) => {
const literal: string = code;
return type({
code: `'${literal}'`
});
};
const createType = <T extends string>(code: T) => {
const literal: string = code;
return type({
code: `'${literal}'`
});
};
With this I'm trying to create types with literals but I don't know those literals until runtime. If I run:
const TypeONE = createType("ONE");
const TypeONE = createType("ONE");
It creates type:
const TypeONE: Type<{
code: string;
}, {}>
const TypeONE: Type<{
code: string;
}, {}>
But instead what I'm actually wanting is:
const TypeONE = type({
code: "'ONE'"
});
const TypeONE = type({
code: "'ONE'"
});
Which produces the correct type:
const TypeONE: Type<{
code: "ONE";
}, {}>
const TypeONE: Type<{
code: "ONE";
}, {}>
23 Replies
ssalbdivad
ssalbdivad8mo ago
Try:
code: `'${literal}'` as const
code: `'${literal}'` as const
This is a good approach but also keep in mind I'll be shipping native generics soon to make this more intuitive
SynthLuvr
SynthLuvrOP8mo ago
as const still causes it to be string I'll try narrow and pipe
ssalbdivad
ssalbdivad8mo ago
Ahh oh well I wasn't sure. Let me take a look I know I did some exploration of which strategies like this TS liked Oh wait You reassigned to string That is why it doesn't work
SynthLuvr
SynthLuvrOP8mo ago
const createType = <T extends string>(code: T) =>
type({
code: type("string")
.narrow((s) => s == code)
.pipe((s) => s as T),
});

const TypeONE = createType("ONE");
const createType = <T extends string>(code: T) =>
type({
code: type("string")
.narrow((s) => s == code)
.pipe((s) => s as T),
});

const TypeONE = createType("ONE");
This produces:
const TypeONE: Type<{
code: (In: string.narrowed) => Out<"ONE">;
}, {}>
const TypeONE: Type<{
code: (In: string.narrowed) => Out<"ONE">;
}, {}>
ssalbdivad
ssalbdivad8mo ago
Realistically '${string}' could validate but doesn't currently That would be the easiest thing For now you can just do this to work around it:
const createType = <T extends string>(literal: T) =>
type({
code: `'${literal}'` as type.cast<T>
})
const createType = <T extends string>(literal: T) =>
type({
code: `'${literal}'` as type.cast<T>
})
Even without the cast it actually is inferred correctly it just doesn't like the validation so you could also ignore the error or something if you wanted When you reassign a variable with an explicit type like this:
const literal: string = code;
const literal: string = code;
Whatever previous narrowed type it had is erased, in this case the association with the generic parameter
SynthLuvr
SynthLuvrOP8mo ago
Ok yes that works
ssalbdivad
ssalbdivad8mo ago
I guess I can't guarantee '${string}' is valid because it could have single quotes Meh
SynthLuvr
SynthLuvrOP8mo ago
Seems to behave differently within scope:
const createType = <T extends string>(code: T) => {
const items = scope({
Item: {
code: `'${code}'` as type.cast<T>,
},
Items: "Item[]>0",
}).export().Items;
return items;
}

const TypeONE = createType("ONE");
type TypeONE = typeof TypeONE.infer;
const createType = <T extends string>(code: T) => {
const items = scope({
Item: {
code: `'${code}'` as type.cast<T>,
},
Items: "Item[]>0",
}).export().Items;
return items;
}

const TypeONE = createType("ONE");
type TypeONE = typeof TypeONE.infer;
This type is valid:
type TypeONE = {
code: "ONE";
}[]
type TypeONE = {
code: "ONE";
}[]
However, this doesn't work:
const createType = <T extends string>(code: T) => {
const items = scope({
Item: {
code: `'${code}'` as type.cast<T>,
},
Items: "Item[]>0",
}).export().Items;

type TypeONE = typeof items.infer;
}
const createType = <T extends string>(code: T) => {
const items = scope({
Item: {
code: `'${code}'` as type.cast<T>,
},
Items: "Item[]>0",
}).export().Items;

type TypeONE = typeof items.infer;
}
It becomes:
type TypeONE = {
code: _distill<T, "out", "base">;
}[]
type TypeONE = {
code: _distill<T, "out", "base">;
}[]
ssalbdivad
ssalbdivad8mo ago
It still looks right to me, it doesn't make sense to have the type be something specific inside the function
const createType = <T extends string>(code: T) => {
const items = scope({
Item: {
code: `'${code}'` as type.cast<T>
},
Items: "Item[]>0"
}).export().Items

type TypeONE = typeof items.infer
return items({})
}

const r = createType("foo")

// R is typed as

type R = ArkErrors | {
code: "foo";
}[]
const createType = <T extends string>(code: T) => {
const items = scope({
Item: {
code: `'${code}'` as type.cast<T>
},
Items: "Item[]>0"
}).export().Items

type TypeONE = typeof items.infer
return items({})
}

const r = createType("foo")

// R is typed as

type R = ArkErrors | {
code: "foo";
}[]
SynthLuvr
SynthLuvrOP8mo ago
hmm. It's working for me without the scope, but I'm getting type error when using scope:
Type '{ message: string; code: T; }[]' is not assignable to type '{ message: string; code: _distill<T, "out", "base">; }[]'.
Type '{ message: string; code: T; }[]' is not assignable to type '{ message: string; code: _distill<T, "out", "base">; }[]'.
ssalbdivad
ssalbdivad8mo ago
You'll have to send a full repro I don't see a point in creating that intermediate type in the inner function
SynthLuvr
SynthLuvrOP8mo ago
const example = <T extends string>(code: T) => {
const Item = type({
code: `'${code}'` as type.cast<T>,
});
type Item = typeof Item.infer;

const createItem = (): Item => {
return { code };
};

const Items = scope({
Item,
Items: "Item[]>0",
}).export().Items;
type Items = typeof Items.infer;

const createItems = (): Items => {
return [{ code }];
};
};
const example = <T extends string>(code: T) => {
const Item = type({
code: `'${code}'` as type.cast<T>,
});
type Item = typeof Item.infer;

const createItem = (): Item => {
return { code };
};

const Items = scope({
Item,
Items: "Item[]>0",
}).export().Items;
type Items = typeof Items.infer;

const createItems = (): Items => {
return [{ code }];
};
};
ssalbdivad
ssalbdivad8mo ago
I don't know why TS simplifies it when using Type but not Scope, but it doesn't seem like something that could be changed externally. I'd also say if you're working with generics and nested functions this complex it's probably best to not worry about casting too much as even with no dependencies involved TS has a lot of limitations in that regard Maybe the example is more complex so you need the nested scope in the function, but if not you could also consider just using chaining and composition to build your types E.g. in a case like this you could use:
const Items = Item.array().atLeastLength(1)
const Items = Item.array().atLeastLength(1)
SynthLuvr
SynthLuvrOP8mo ago
I don't need the explicit return type there, I was only adding it to make sure my function was returning the correct type. If I infer it after creation then it's as I expect:
type Items = ReturnType<typeof createItems>;
type Items = ReturnType<typeof createItems>;
For some reason it doesn't think the type types match when they do
ssalbdivad
ssalbdivad8mo ago
Yeah best not to worry about it haha Like worry the external result is correct But trying to get correct inference in those intermediate steps will not always be possible
SynthLuvr
SynthLuvrOP8mo ago
Ok so it's not related to scope, because even Item.array().atLeastLength(1) is throwing the error. It must have something to do with the array
ssalbdivad
ssalbdivad8mo ago
I see. This is not a real external issue though right? You can just return your type and it is inferred as expected
SynthLuvr
SynthLuvrOP8mo ago
Yes it's not a real problem, I just wanted to do a sanity check on the typing to see if I could get a compile time error if I wrote the return value incorrectly. Run time works as expected
ssalbdivad
ssalbdivad8mo ago
I think I know why that's happening based on what you mentioned about the array but it's based on the way inputs/outputs and constraints are extracted from the type representation If you wanted to avoid that error internally you'd have to use type.cast on the array as well, but I wouldn't do that I'd just let the type infer on its own it will be safe that way You can't use the "wrong" return type if you just let it infer itself anyways (or at least if TS is working correctly)
SynthLuvr
SynthLuvrOP8mo ago
This is definitely over-engineering haha, but I can get the compile-time checks working with inferences. First create an AssertEqual type:
type IsEqual<T, U> = (<G>() => G extends T ? 1 : 2) extends <G>() => G extends U
? 1
: 2
? true
: false;
type AssertEqual<T, U> = IsEqual<T, U> extends true ? T : never;
type IsEqual<T, U> = (<G>() => G extends T ? 1 : 2) extends <G>() => G extends U
? 1
: 2
? true
: false;
type AssertEqual<T, U> = IsEqual<T, U> extends true ? T : never;
Then compare the type with the inferred type:
type Items = typeof Items.infer;
type createItems = ReturnType<typeof createItems>;
type _ = AssertEqual<Items, createItems>;
type Items = typeof Items.infer;
type createItems = ReturnType<typeof createItems>;
type _ = AssertEqual<Items, createItems>;
If the function differs, then the compiler throws an error Can just add a simple sanity check outside of the function, this works:
const example = <T extends string>(code: T) => {
const Item = type({
code: `'${code}'` as type.cast<T>,
});

const Items = scope({
Item,
Items: "Item[]>0",
}).export().Items;

const createItems = () => {
return [{ code }];
};

return {
createItems,
Items
}
};

// Compile-time sanity check
const test = example("test");
(): typeof test.Items.infer => test.createItems();
const example = <T extends string>(code: T) => {
const Item = type({
code: `'${code}'` as type.cast<T>,
});

const Items = scope({
Item,
Items: "Item[]>0",
}).export().Items;

const createItems = () => {
return [{ code }];
};

return {
createItems,
Items
}
};

// Compile-time sanity check
const test = example("test");
(): typeof test.Items.infer => test.createItems();
This way it can be assured that the types match. If createItems return type doesn't match the type, then an error will be thrown during compile
ssalbdivad
ssalbdivad8mo ago
If you're building stuff complex enough that you need type testing you could also consider attest!
Dimava
Dimava8mo ago
Have you considered just using type('===', code)?
aabad_ankit
aabad_ankit4mo ago
Thank You. This is what I was looking for.

Did you find this page helpful?