I have problem with mapping in keyof. I'm trying to map over Routes type, but when I map over the object then it breaks conditional params for Route.
type Routes = {
'/home': {}
'/pages': {
pageId: number
}
}
type IRoute<RouteName, Params> = {
route: RouteName
} & ({} extends Params ? { params?: Params } : { params: Params })
type Router = {
[RouteName in keyof Routes]: IRoute<RouteName, Routes[RouteName]>
}
type Route = Router[keyof Router]
Here params should be required but TS ignores it:
const foo: Route = {
route: '/pages'
// Missing params: { pageId: number }
}
I need Route type with routeName and params. If params is a generic object then make it optional.
const foo3: IRoute<'/foo', {id: number}> = {
route: '/foo',
params: {
id: 1
}
}
const foo4: IRoute<'/foo', {}> = {
route: '/foo'
}
Here's my code. If you call IRoute it works as I expect. But when IRoute is called from mapping through in keyof it breaks and params is optional for all routes.
Here's a TS playground.
Change your condition from:
{} extends Params ?
to:
keyof Params extends never ?
See the TypeScript Playground.
Related
I would like to create an array tuple with values ['account', 'accountOne'], that are using existing Types, but second tuple value should contain values based on selection of the first one.
Some example code below:
interface RootState {
account: Account;
movies: Movies;
config: Config;
}
interface Account {
accountOne: 'something',
accountTwo: '1'
}
interface Movies {
moviesOne: 'something',
moviesTwo: 'something'
}
interface Config {
configOne: 'something',
configTwo: '123'
}
export type ModuleProp = keyof RootState;
// This is some pseudo code, to show the idea, but I could not make it work
// It gives error: Tuple type arguments circularly reference themselves
export type ModulesTuple = [ModuleProp, keyof RootState[ModulesTuple[0]]
// It would be used as
function fetchSomething({page: number, vuexModuleProp: ModulesTuple}){
const [module, moduleProp] = vuexModuleProp
// rest of the code
}
fetchSomething({
page: 1,
vuexModuleProp: ['movies', 'accountOne'] // ERROR
})
fetchSomething({
page: 1,
vuexModuleProp: ['movies', 'moviesOne'] // GOOD TO GO
})
Current code gives error of: Tuple type arguments circularly reference themselves
The idea is that if you select 'account' as first tuple, second selection should be 'accountOne' or 'accountTwo', which are nested keys of other interfaces (Movies, Account, Config) in RootState interface.
Not really sure if that is possible with Typescript, but would greatly appreciate any help!
Use Generics.
interface RootState {
account: Account;
movies: Movies;
config: Config;
}
interface Account {
accountOne: 'something',
accountTwo: '1'
}
interface Movies {
moviesOne: 'something',
moviesTwo: 'something'
}
interface Config {
configOne: 'something',
configTwo: '123'
}
// All available modules
type ModuleKeys = keyof RootState;
// Pick a module interface based on type of `T`
type ModuleProps<T extends ModuleKeys> = T extends 'account'
? Account
: T extends 'movies'
? Movies
: T extends 'config'
? Config
: never;
// Creates a tuple where the second type is based on the first type, where the first type is automatically inferred from `T`.
type ModuleTuple<T extends ModuleKeys> = [T, keyof ModuleProps<T>];
// Guard the `T` type to be one of `ModuleKeys`, where it is passed to `ModuleTuple<T>`, where it creates the conditional tuple.
function fetchSomething<T extends ModuleKeys>({ page, vuexModuleProp }: { page: number, vuexModuleProp: ModuleTuple<T> }): any {
const [moduleKey, moduleProps] = vuexModuleProp;
console.log(page, moduleKey, moduleProps);
}
fetchSomething({
page: 1,
vuexModuleProp: ['movies', 'accountOne'] // ERROR
});
fetchSomething({
page: 1,
vuexModuleProp: ['movies', 'moviesOne'] // GOOD TO GO
});
Check the playground.
I am trying to type de values of an array in a template object.
Currently I have achieved my goal using objects like so :
// defining the model type
interface RouteModel {
route: string
params?: Record<string, string>
}
interface RoutesModel {
[routeName: string]: RouteModel
}
// value constructor
function makeRoutes<T extends RoutesModel>(input: T) {
return input
}
// type safe creation for routes
const routes = makeRoutes({
potato: {
route: '/potato/:potatoId/rate',
params: { potatoId: '' },
},
grapes: {
route: 'grapes',
},
banana: {
route: 'bag/:bagId/:bananaId',
params: { bagId: '', bananaId: '' },
},
})
const useTypedHistory = <T extends RoutesModel>() => {
const navigate = <K extends keyof T>(route: K, params: Record<keyof T[K]['params'], string>) => {
}
return { navigate }
}
const Component = () => {
const { navigate } = useTypedHistory<typeof routes>()
navigate('banana', { bagId: '123', bananaId: '567' })
ʌ --- type safety works here, it requires the right object depending on the first param
return null
}
export default useTypedHistory
My problem is that at the beginning, I declare my params as an object with the correct keys but empty string to make it work.
I would like to an array of strings instead, so it would look like this:
// defining the model type
interface RouteModel {
route: string
params?: string[]
}
interface RoutesModel {
[routeName: string]: RouteModel
}
// value constructor
function makeRoutes<T extends RoutesModel>(input: T) {
return input
}
// type safe creation for routes
const routes = makeRoutes({
potato: {
route: '/potato/:potatoId/rate',
params: ['potatoId'],
},
grapes: {
route: 'grapes',
},
banana: {
route: 'bag/:bagId/:bananaId',
params: ['bagId', 'bananaId'],
},
})
const useTypedHistory = <T extends RoutesModel>() => {
const navigate = <K extends keyof T>(route: K, params: Record<(valueof T[K]['params']), string>) => {
ʌ --- does not work
}
return { navigate }
}
But that doesn't work at all. From what I've seen, valueof would allow me to infer the values, but I can't seem to make it work.
Any help would be very much appreciated
My question is about my update method. As you can see below, it can receive an object(newState), and updates the class instance properties using Object.assign(). I need to tell TS that it should only accept:
An object
Properties that are keys of the State Class
Values of those properties that are proper types of the properties.
Did I type this method correctly? Is there a better//other way to do it?
Also, in main.ts, on the State class implements StateInterface, the TS compiler has an error that the parameter for update (newState) is implicitly any. Shouldn't it be receiving the type information from types.d.ts?:
/// types.d.ts
export interface StateInterface {
user: User;
fileList: DirectoryResponse;
selectedFiles: Array<SelectedFile>;
currentDir: string;
response: APIResponse;
menu: Menu;
dialog: Dialog;
history: object;
update: <P extends StateInterface, T extends keyof StateInterface>(newState: { [key in T]: P[T]}) =>
Promise<void>;
syncPage: () => void;
}
/// main.ts
class State implements StateInterface {
user: User;
fileList: DirectoryResponse;
selectedFiles: SelectedFiles;
currentDir: string;
response: APIResponse;
menu: Menu;
dialog: Dialog;
history: History;
constructor(user: User, fileList: DirectoryResponse, selected: SelectedFiles, currentDir: string, response: APIResponse, menu: Menu, dialog: Dialog, history: History = { forward: false, back: false }) {
this.user = user;
this.fileList = fileList;
this.selectedFiles = selected.slice();
this.currentDir = currentDir;
this.response = response || { fileResults: [], folderResults: [] };
this.menu = menu || { location: '', type: 'folder' };
this.dialog = dialog || { type: "", state: false };
this.history = history;
}
get dir() {
return this.currentDir.slice(1).split('/');
};
async update(newState): Promise<void> {
^^^^^^^^ (implicit any)
if (newState) {
Object.assign(this, newState);
}
this.fileList = await readDir(this.currentDir).then(r=>r.json());
}
}
The way you typed StateInterface suggests you only want keys of StateInterface in the newState (and not other properties possibly existing in State).
If this is the case, I would type update in both interface and class as
update(newState: Partial<StateInterface>): void {
...
}
Also note that this allows functions existing in the StateInterface to be replaced, younprobably want to use Omit to get rid of unwanted keys.
I have data structure like this:
const endpoints = {
async Login: (params) => { ... }
async Register: (params) => { ... }
}
Now I want to specify that every item in this object must accept params object and return a promise.
I can do something like this:
interface EndpointMap {
[endpointName: string]: (
params: Record<string, any>
) => Promise<any>;
}
This works well. But there's a downside to this.
If I do elsewhere that for example endpoint : keyof typeof endpoints the result would only be string. If I remove the EndpointMap interface, I'd get a String Union of all the keys on the endpoint object. Much better!
Is there a way to have the best of both worlds?
Thanks!
You can achieve this by creating endpoints using identity function:
interface EndpointMap {
[endpointName: string]: (
params: Record<string, any>
) => Promise<any>;
}
const createEndpoints = <TMap extends EndpointMap>(map: TMap) => map;
const endpoints = createEndpoints({
login: async (params) => ({}),
register: async (params) => ({})
});
/*
type of 'endpoints' variable is:
{
login: (params: Record<string, any>) => Promise<{}>;
register: (params: Record<string, any>) => Promise<{}>;
}
*/
We've declared identity function with EndpointMap constraint on generic type parameter so typescript will verify that passed parameter has appropriate structure. Additionally parameter type will be inferred and valid keys' type won't be widened to string, so:
type Keys = keyof typeof endpoints; // will be "login" | "register"
Playground
One way to achieve this is instead of giving keys as type string define the keys in the type EndpointMap:
type EndpointMap = {
[key in 'Login' | 'Register']: (
params: any
) => Promise<any>
}
const endpoints: EndpointMap = {
Login: (params) => { ... },
Register: (params) => {... }
}
// Valid
const oneKey: keyof typeof endpoints = 'Login'
// Type '"Random"' is not assignable to type '"Login" | "Register"'
const otherKey: keyof typeof endpoints = 'Random'
I have this interface:
export interface IScene<R extends string> {
path: R;
params?: SceneParams;
}
SceneParams interface:
export interface SceneParams {
[key: string]: string;
}
This works totally fine when I create a scene like:
interface PostDetailScene extends IScene<PostRoute.Detail> {
params: PostDetailSceneParams;
}
PostDetailSceneParams:
export interface PostDetailSceneParams extends SceneParams {
postId: string;
}
All these code gets correct type checking:
// CORRECT
getPathWithParams({
path: PostRoute.Detail,
params: { postId: '4' },
});
getPathWithParams({
path: UserRoute.List,
});
getPathWithParams({
path: UserRoute.List,
params: undefined,
});
// ERROR
getPathWithParams({
path: PostRoute.Detail,
params: undefined,
});
getPathWithParams({
path: PostRoute.Detail,
});
getPathWithParams({
path: UserRoute.List,
params: { wrongParam: 'value' },
});
Now I have a scene where I don't want to pass any props. This scene is the UserListScene:
interface UserListScene extends IScene<UserRoute.List> {
params?: never;
}
You see I have to explicitly type params?: never (or params?: undefined - I also don't know which type I should use here because here the params would/should really never get passed - but with never the compiler gets also satisfied with undefined so I don't see that much difference)
My question is: Is there a solution for changing the IScene interface so that I don't have to type params?: never or params?: undefined when there are no params for this scene?
I just want to write:
interface UserListScene extends IScene<UserRoute.List> {}
or:
type UserListScene = IScene<UserRoute.List>;
EDIT:
This function should also get correct type checking:
export function getPathWithParams(scene: Scene): string {
if (!scene.params) {
return scene.path;
}
let pathWithParams: string = scene.path;
const paramsAsString = Object.keys(scene.params);
paramsAsString.forEach((param: string) => {
pathWithParams = pathWithParams.replace(`:${param}`, scene.params[param]);
});
return pathWithParams;
}
Use two interfaces (as truly that is what you really have here):
export interface IScene<R extends string> {
path: R;
}
export interface ISceneWithParams<R extends string> {
path: R;
params: SceneParams;
}